Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

typescript always return a intersection even though I specify the generic #47749

Closed
carl-jin opened this issue Feb 6, 2022 · 3 comments
Closed

Comments

@carl-jin
Copy link

carl-jin commented Feb 6, 2022

Bug Report

I trying to create a function that will return a different instance of class by passing different args ,

but when I specify the generic of args, typescript always return an intersection instead of union;

🔎 Search Terms

generic

🕗 Version & Regression Information

typescript: 4.3.5
Same behavior in 4.5.4

⏯ Playground Link

Playground link with relevant code

💻 Code

type ClassAOptions = {
  fooA: string;
  barA: string;
};
class ClassA {
  constructor(options: ClassAOptions) {}

  static new(options: ClassAOptions): ClassA {
    return new ClassA(options);
  }
}

type ClassBOptions = {
  fooB: boolean;
  barB: boolean;
};
class ClassB {
  constructor(options: ClassBOptions) {}

  static new(options: ClassBOptions): ClassB {
    return new ClassB(options);
  }
}

const classList = {
  first: ClassA,
  second: ClassB,
};

function createClass<K extends keyof typeof classList>(
  className: K,
  args: ConstructorParameters<typeof classList[K]>[0]
) {
  //  type error
  //  Argument of type 'ClassAOptions | ClassBOptions' is not assignable to parameter of type 'ClassAOptions & ClassBOptions'.
  return new classList[className](args);
}

createClass("first", { fooA: "false", barA: "false" });
createClass("second", { fooB: false, barB: false });

🙁 Actual behavior

args return an intersection ( ClassAOptions & ClassBOptions )

🙂 Expected behavior

args should be an Union ( ClassAOptions | ClassBOptions )

@MartinJohns
Copy link
Contributor

This is working as intended.

You're wrongly assuming K always refers to a single specific key, but it can be a union as well: "first" | "second". You would need #35873 or #27808 for this to work.

@carl-jin
Copy link
Author

carl-jin commented Feb 6, 2022

#27808

yep, that makes sense , ty

@carl-jin carl-jin closed this as completed Feb 6, 2022
@craigphicks
Copy link

craigphicks commented Feb 6, 2022

I don't believe that #27808 covers your issue. #27808 is concerned with presenting the correct calling interface that rejects illegal calls. In your code that is already miraculously working -

createClass("first", { fooB: false, barB: false });  // error correct
createClass("second", { fooA: "false", barA: "false" });  // error correct

Your description of expected and actual behavior is a bit confusing to me.

I see the actual behavior to be that TS shows args as type ClassAOptions | ClassBOptions.

It also actually complains that args should be type ClassAOptions & ClassBOptions.
But ClassAOptions & ClassBOptions is actually a nonsense type -

{fooA:string,barA:string,fooB:boolean,barB:boolean}

When strictly checked it requires all four properties:

function foo(a:ClassAOptions & ClassBOptions){};
foo({fooA:"false",barA:"false",fooB:false,barB:false}) // not an error
foo({fooA:"false",barA:"false"}) // error, missing properties

It is just lucky circumstance (or careful planning) that it doesn't fail in createClass - because the property names all differ.
If they didn't the & type would become never and always cause an error.

If the type-checker went through case of allowed parameter list types and performed a separate type-check for each one, then your error would not occur. The are good reasons for not doing that always - it takes more time.
I addressed this in issue #47580, which is actually just a long winded way to say "under limited circumstances type-checker should go through each case of allowed parameter list types and performed a separate type-check for each one". In your case there are already two known valid cases

createClass("first", { fooA: string, barA: string });  // pass
createClass("second", { fooB: boolean, barB: boolean });  // pass

and two known invalid cases

createClass("first", { fooB: boolean, barB: boolean });  // error correct
createClass("second", { fooA: string, barA: string });  // error correct

so it would just type-check to two valid cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants