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
{{ message }}
Repository navigation
(Indirect) inference of array element type in callback function type does not work #40743
Search Terms: array element type inference callback
Code
Found a problem with type inference when tried to implement functions and array methods taking callbacks, where the array element type will be inferred from the actual array type.
What I have:
a helper type ElemT that extracts the element type from an array type.
a function type FuncT to be used in array iteration functions.
a global function taking an array and a callback.
a new array class with a method taking a callback.
The callback wants to pass the array with its actual type as parameter. Therefore, FuncT needs the array type as generic parameter, and must infer the element type for its first parameter.
// helper type to infer element type from array typetypeElemT<ATextendsany[]>=ATextends(infer T)[] ? T : never;// callback function taking element type and array typetypeFuncT<ATextendsany[]>=(v: ElemT<AT>,a: AT)=>void;// global functionfunctionexample<ATextendsany[]>(arr: AT,cb: FuncT<AT>){cb(arr[0],arr);// <==== works}// class methodclassMyArr<T>extendsArray<T>{publicexample(cb: FuncT<this>){cb(this[0],this);// <==== fails}}
However, I fond a workaround: When I bake the ElemT into the FuncT it works as expected. But that's a little inconvenient because this has to be done for multiple callback types in my real code.
// callback function taking element type and array typetypeFuncT<ATextendsany[]>=ATextends(infer T)[] ? (v: T,a: AT)=>void : never;// global functionfunctionexample<ATextendsany[]>(arr: AT,cb: FuncT<AT>){cb(arr[0],arr);// <==== works}// class methodclassMyArr<T>extendsArray<T>{publicexample(cb: FuncT<this>){cb(this[0],this);// <==== works now}}
Expected behavior:
In MyArr#example, ElemType<this> resolves to T.
Actual behavior:
In MyArr#example, this[0] (with type T) cannot be assigned to ElemT<this>.
It’s incredibly difficult to understand/explain why, but this is basically working as intended. In fact, we believe that your workaround example maybe shouldn’t work if we wanted to be totally safe. The TL;DR is I think your code is safe in practice, but only because of constraints that the compiler can’t/doesn’t track. The more abstract patterns the compiler sees here are not safe in general. The proper solution is to replace ElemT<AT> with AT[number]—you don’t need a conditional type to get the element type of an array type.
The longer, more complicated answer, is that the this type is actually a type parameter (because it gets instantiated with different types in different subclasses), which means that the conditional type ElemT<this> gets deferred—it doesn’t want to give you T right away, because a subclass will have a different this and therefore a different element type. Now, you know that if you subclass MyArr<T>, the element type of that subclass will necessarily be assignable to T (the element type of MyArr<T>), because T is covariant in MyArr<T>. But I’m not sure the compiler knows that about all possible instantiations of this. Even if it did know that, it would also have to know that all possible instantiations of this would always pass inference in AT extends (infer T)[] and take the true branch of that conditional. This is another fact that appears clear to you as a human, since you are not able to remove the array-ness of a subclass of Array, but this is another fact that may not be encoded into the compiler (it seems like it would have to be a special case, and/or would be a lot of work to determine). The final gotcha is that ElemT<AT> is a distributive conditional type, which means it can have non-linear assignability if AT is a union type. Unresolved distributive conditional types have no assignability relationships. (Actually, I believe the current state is that any unresolved conditional type has no assignability relationships—see #30639 and #27932.) Yet again, it appears clear that a subclass cannot have a this type that’s a union, but I don’t think the checker rules that out to create a special exception here—it just sees a distributive conditional type and says “nope.”
Activity
andrewbranch commentedon Sep 25, 2020
It’s incredibly difficult to understand/explain why, but this is basically working as intended. In fact, we believe that your workaround example maybe shouldn’t work if we wanted to be totally safe. The TL;DR is I think your code is safe in practice, but only because of constraints that the compiler can’t/doesn’t track. The more abstract patterns the compiler sees here are not safe in general. The proper solution is to replace
ElemT<AT>withAT[number]—you don’t need a conditional type to get the element type of an array type.The longer, more complicated answer, is that the
thistype is actually a type parameter (because it gets instantiated with different types in different subclasses), which means that the conditional typeElemT<this>gets deferred—it doesn’t want to give youTright away, because a subclass will have a differentthisand therefore a different element type. Now, you know that if you subclassMyArr<T>, the element type of that subclass will necessarily be assignable toT(the element type ofMyArr<T>), becauseTis covariant inMyArr<T>. But I’m not sure the compiler knows that about all possible instantiations ofthis. Even if it did know that, it would also have to know that all possible instantiations ofthiswould always pass inference inAT extends (infer T)[]and take the true branch of that conditional. This is another fact that appears clear to you as a human, since you are not able to remove the array-ness of a subclass ofArray, but this is another fact that may not be encoded into the compiler (it seems like it would have to be a special case, and/or would be a lot of work to determine). The final gotcha is thatElemT<AT>is a distributive conditional type, which means it can have non-linear assignability ifATis a union type. Unresolved distributive conditional types have no assignability relationships. (Actually, I believe the current state is that any unresolved conditional type has no assignability relationships—see #30639 and #27932.) Yet again, it appears clear that a subclass cannot have athistype that’s a union, but I don’t think the checker rules that out to create a special exception here—it just sees a distributive conditional type and says “nope.”danielrentz commentedon Sep 25, 2020
Thank you very much for this comprehensive explanation. Indeed, I forgot about the T[number] syntax, I will give that a try in my code.
typescript-bot commentedon Sep 27, 2020
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.