You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// Use of the conditional type `string extends T` prevents assignability.// However, the conditional type `T extends string` is accepted.declareclassFoo<T>{bar: stringextendsT ? number : boolean;}declareletindexable: Foo<{[key: string]: boolean}>;declareletnamedProp: Foo<{baz: boolean}>;indexable=namedProp;
🙁 Actual behavior
When a conditional type is present in Foo, I sometimes cannot assign Foo<{a: any}> to Foo<[x: string]: any>, depending on the condition.
🙂 Expected behavior
Regardless of the conditional type, I should be able to assign Foo with named properties to Foo with an index signature.
It's very possible a straightforward fix here (if one is even possible) could have side effects that result in the fix being rejected. I'd recommend designing around it if possible.
After walking through the compiler a bit, it does seem to be marking type extends T's case as invariant like you said, while T extends type's case gets marked as bivariant.
Is the expected behavior for type extends T to be flagged as bivariant like T extends type? Although it might satisfy the above test case, I think that it would probably lead to a weird behavior similar to how bivariance currently interacts with T extends type, for example:
declareclassA{a: string;}declareclassBextendsA{b: string;}declareclassFoo<T>{foo: TextendsB ? number : boolean}declareletfooA: Foo<A>;declarelettempA: Foo<A>;declareletfooB: Foo<B>;fooA.foo;// booleanfooB.foo;// numbertempA=fooA;fooA=fooB;fooB=tempA;fooA.foo;// incorrectly booleanfooB.foo;// incorrectly number
Or should we try something else, like somehow applying a structural comparison between the classes when we encounter those kinds of conditional types with generics? For example, below I use a duplicate of class Foo<T> to cause the compiler to perform a structural comparison instead of checking type parameters variance:
declareclassA{a: string;}declareclassBextendsA{b: string;}declareclassFoo<T>{foo: BextendsT ? number : boolean}declareclassBoo<T>{foo: BextendsT ? number : boolean}declareletfooA: Foo<A>;declarelettempA: Foo<A>;declareletfooB: Boo<B>;tempA=fooA;// both assignment works because fooA.foo and fooB.foo are both number// even though the type parameters aren't bivariantfooA=fooB;fooB=tempA;
Here's a more robust and extremely surprising example of what can happen as a result of this invariance, in particular as it interacts with boolean widening. Playground link.
Code:
class FormArray<T extends FormControl<any>> {
constructor(public readonly controls: Array<T>) {}
// When T is on the RHS of a conditional type, FormArray<T> becomes invariant in T.
// https://github.com/microsoft/TypeScript/issues/48033
foo!: string extends T ? true : false;
}
class FormControl<T> {
constructor(public readonly value: T) {}
}
function test() {
// Notice that TypeScript infers this as `FormControl<boolean>` -- there are some
// (likely special-cased) rules for inferring `boolean` instead of the narrow literal type `true`
const fc1 = new FormControl(true);
// Here, explicitly specifying the boolean on the LHS blocks the special casing above,
// causing the RHS constructor to be inferred as `FormControl<true>`.
const fc2: FormControl<boolean> = new FormControl(true);
// This infers as `FormArray<FormControl<boolean>>`, as above.
const fa1 = new FormArray([new FormControl(true)]);
// Error! Again, providing an explicit type causes the RHS control to be inferred
// as `FormControl<true>` instead of `FormControl<boolean>`. Because `FormArray<T>`
// is invariant in T, `FormArray<FormControl<true>>` is not assignable to `FormArray<FormControl<boolean>>`.
const fa2: FormArray<FormControl<boolean>> = new FormArray([new FormControl(true)]);
}
interfaceM{A: number;B: number;}declareclassFoo<TextendskeyofM>{bar: M[T]}declareletfoo1: Foo<"A">;letfoo2: Foo<"B">=foo1;// "A" is not assignable to "B"
Ryan Cavanaugh (@RyanCavanaugh), agreed, I'd expect an assignability error between the two Foos in your example. But in mine, Foo<"A">["bar"] and Foo<"B">["bar"] are both number, so intuitively I'd expect the Foos to be fungible. Is it perhaps the case that checker doesn't look through the type argument to do a structural check on the individual members?
Types are not evaluated by iterating over each possible thing the type could be inhabited by, since doing things that way is combinatorially explosive. Things are generally either true for a category, or not, and an operation like this isn't valid at a category level.
Activity
RyanCavanaugh commentedon Feb 26, 2022
Likely we're considering a
type extends Tto markFoo<T>as invariant inTdylhunn commentedon Feb 26, 2022
Is this on a roadmap or existing issue? I'd like to know whether to wait for this fix, or design around it. Thanks!
dylhunn commentedon Feb 26, 2022
(More context: we'd be using this as part of the solution for this longstanding Angular issue.)
RyanCavanaugh commentedon Feb 28, 2022
It's very possible a straightforward fix here (if one is even possible) could have side effects that result in the fix being rejected. I'd recommend designing around it if possible.
jihndai commentedon Mar 2, 2022
After walking through the compiler a bit, it does seem to be marking
type extends T's case as invariant like you said, whileT extends type's case gets marked as bivariant.Is the expected behavior for
type extends Tto be flagged as bivariant likeT extends type? Although it might satisfy the above test case, I think that it would probably lead to a weird behavior similar to how bivariance currently interacts withT extends type, for example:Or should we try something else, like somehow applying a structural comparison between the classes when we encounter those kinds of conditional types with generics? For example, below I use a duplicate of
class Foo<T>to cause the compiler to perform a structural comparison instead of checking type parameters variance:dylhunn commentedon Mar 31, 2022
Here's a more robust and extremely surprising example of what can happen as a result of this invariance, in particular as it interacts with
booleanwidening. Playground link.Code:
yseymour commentedon Aug 2, 2022
This also affects mapped types:
RyanCavanaugh commentedon Aug 2, 2022
Yudhi Seymour (@yseymour) that's a correct error. When
M[T]is anything other than a roundabout way of writingnumber, the assignment is unsafeyseymour commentedon Aug 3, 2022
Ryan Cavanaugh (@RyanCavanaugh), agreed, I'd expect an assignability error between the two
Foos in your example. But in mine,Foo<"A">["bar"]andFoo<"B">["bar"]are bothnumber, so intuitively I'd expect theFoos to be fungible. Is it perhaps the case that checker doesn't look through the type argument to do a structural check on the individual members?Maybe this is a different issue to the OP.
RyanCavanaugh commentedon Aug 5, 2022
Types are not evaluated by iterating over each possible thing the type could be inhabited by, since doing things that way is combinatorially explosive. Things are generally either true for a category, or not, and an operation like this isn't valid at a category level.
mkantor commentedon Nov 13, 2024
Here's another example of this behavior which caught me by surprise:
EDIT: Mapped/index-signatured types behave similarly.