-
Notifications
You must be signed in to change notification settings - Fork 12.4k
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
Conditional parameter type based on generic type has inconsistent behavior when generic type passed as generic argument #46155
Comments
I was gonna report this as a bug, but I feel like this is probably related to your issue -- // Evaluates to any if T is assignable to U, and never otherwise
type Assignable<T, U> = T extends U ? any : never;
// string | null is apparently assignable to string..?
type A = Assignable<string | null, string>;
// Oh wait, this one properly evaluates to never...
type B = (string | null) extends (string) ? any : never; Basically, if you hover over |
@Codesmith1024 They're not doing the same check. So basically your type evaluates like this:
If this behaviour is undesirable in your case, the documentation provides an example on how to opt-out from this feature. |
Oh gotcha, thanks for the concise link to the docs and an explanation! |
@Hookyns There's absolutely no way for TypeScript to figure out what Due to this you're wrongly assuming the parameters of the function are either |
To elaborate on the above: My understanding is, when a conditional type is instantiated with a generic type parameter, its evaluation is deferred and assignability to it is determined by an internal check This particular check can only be satisfied by another conditional type. |
@MartinJohns Even if I use constraint, so it cannot be void (so false branch should be used), it does not work. Minimal example: function foo<T = void>(arg: T extends void ? true : false): T | undefined
{
return undefined;
}
function callFoo<T extends boolean>()
{
foo<T>(true); // error, boolean is not assignable...
foo<T>(false); // same error
}
callFoo<true>(); So @fatcerberus seems be right. Updated example which is a little more complex but closer to my case. Playground class SomeType
{
public readonly ctor: Function;
constructor(...) { ... }
}
// https://github.com/Hookyns/ts-reflection
function getType<T = void>(..._: T extends void ? ["You must provide a type parameter"] : []): SomeType // <----------
{
return new SomeType({ ctor: Object, /* and more information */});
}
class ServiceProvider
{
getService<TDependency extends IService>(t?: SomeType): TDependency // <--- even with constraint; now it cannot be void, never
{
t = t ?? getType<TDependency>(); // Not working here! <----------
return Reflect.construct(t.ctor, []);
}
}
interface IService
{
foo(): void;
}
class SomeService implements IService
{
constructor() {}
foo() {}
}
const sp = new ServiceProvider();
const someService = sp.getService<SomeService>();
someService .foo();
getType<SomeService>(); // Working here! <----------
getType(); // Correct error, because required generic parameter is missing! |
It works here as you expect, because TypeScript knows exactly what type you're dealing with:
It doesn't work here, because TypeScript doesn't know what type you're having here. And constraints are not considered for this. An actual type is needed. Here's an example to hopefully help you understand the issue: type Test<T> = T extends number ? true : false
function example1<T extends number>() {
// The type is neither "true" nor "false", because the provided type argument is generic.
// As a result "Actual" is still typed "T extends number ? true : false"
type Actual = Test<T>
}
function example2(value: number) {
// Here the type argument is not generic, but actually known: number
// As a result the conditional type can be resolved to "true".
type Actual = Test<typeof value>
} |
Thank You @MartinJohns for explanation what generic means. 😑
So it sounds like TypeScript can and should do that, but TypeScript don't do that. What is the definition of bug, please? Doesn't matter if it is difficult to implement or not. Is it valid? Should it work? Yes! Or no? Tell me it is a bug and I can invest my time to try to figure it out and maybe make a PR. Just don't say it is working like a charm and it is intended behavior because You don't want to implement it. Just say OK, it is a bug but it will be almost impossible to implement that. Then I will be happy. And what about this? If TypeScript can report error for function getType<T = undefined>(arg: T extends undefined ? true : false)
{
}
function callFoo<T extends boolean>()
{
const x: T = undefined; // Error, 'T' is not assignable to type T. Interesting.. TypeScript know what T is, that it cannot be undefined.
getType<T>(false); // Ohh.. error... it may be undefined.. Sounds like BUG for me.
} PS: Sorry for my sarcasm, but it seems like You are trying to throw everything, no matter what, just to, idk, hide a bug. |
It’s not a bug. TS can’t resolve the conditional type because it doesn’t know what the type is inside the generic function. The caller has to provide it. |
function foo<T = void>(arg: T extends void ? true : false): T | undefined
{
return undefined;
}
function callFoo<T extends boolean>()
{
foo<T>(true); // error
foo<T>(false); // hypothetical universe where this doesn't error
}
callFoo<never>() // ok; never extends boolean
foo<never>(false) // this errors because never extends void
// note that this didn't error in the `callFoo` invocation |
I'm not part of the TypeScript team, and I'm not trying to hide anything. I was merely trying to explain you why it's not a bug and you're mistaken. But at some point I have to accept that I can't explain it any more simple, so I'll excuse myself from this issue. |
@Hookyns It feels like you are expecting modus tollens to apply to the evaluation of the conditional types. If the value level argument is the literal However, this just isn't how conditional types are evaluated, changing this would be a very signficant breaking change, and changing this would introduce signficant structure comparsion checks that would create an performance overhead that would make the conditional type feature unusable. Effectively conditional types only support modus ponens. |
Thank You @m-rutter , this makes more sense. I was expecting that ternary operator in conditional types works like ternary operators, which is I will check the implementation (see you next year 😄), because my idea of implementation (that // callExpression is foo<T>(false)
isTypeIdenticalTo(
getType(callExpression.arguments[0]) /* "false" */,
getType(callExpression.declarations[0].parameters[0]) /* T extends undefined ? true : false */,
callExpression.typeArguments /* T extends boolean, to let the isTypeIdenticalTo function know what T is */
) And the 3rd argument of |
To be fair, it would be more accurate to say that the evaluation of the conditional type is deferred, because actually evaluating such things in a generic context requires higher-level reasoning than the compiler is capable of. Basically, we know that the code inside the body of And as @m-rutter pointed out, TS doesn't do modus tollens inferences. That kind of reasoning would require negated types - see #29317. The compiler can do inferences of the form |
Bug Report
🔎 Search Terms
generic conditional parameter type inconsistent behavior
I tried to search a lot of terms but I don't even know how to name this problem.
🕗 Version & Regression Information
⏯ Playground Link
Playground link with relevant code
💻 Code
🙁 Actual behavior
Call of
getType<T>()
function with generic parameter as generic argument does not work the same way as call with specific typegetType<Bar>()
.🙂 Expected behavior
Both cases should work the same way.
The text was updated successfully, but these errors were encountered: