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
Mapped array/tuple types can't be used as generic with a constraintΒ #48740
classValType{
#isValType =true;}classI32extendsValType{
#isI32 =true;}classExternrefextendsValType{
#isExternref =true;}classLocal<TextendsValType>{readonly #type: T;constructor(type: T){this.#type =type;}gettype(): T{returnthis.#type;}}typeLocalsFor<TypeextendsReadonlyArray<ValType>>={// Type 'Type[K]' does not satisfy the constraint 'ValType'.// Type 'Type[keyof Type]' is not assignable to type 'ValType'.// Type 'Type[string] | Type[number] | Type[symbol]' is not assignable to type 'ValType'.// Type 'Type[string]' is not assignable to type 'ValType'.(2344)[KinkeyofType]: Local<Type[K]>};// This is still the correct type, because mapped array/tuples still// just map their values despite the above errorconstlocals: LocalsFor<[I32,Externref]>=[newLocal(newI32()),newLocal(newExternref()),];
π Actual behavior
It produces an error in the mapped type that Type[K] doesn't satisfy the constraint.
π Expected behavior
There should be no error, as mapped types do not map over all keys in the array/tuple, it should only consider Type[K] to be the numeric keys of the tuple, i.e. Type[K] in the mapped type will only ever be (subtypes of) ValType because ultimately K will only be assigned to the numeric keys of Type as it's an array type.
In this particular case, we have an explicit Type extends ReadonlyArray<ValType> so TypeScript should be able to deduce just fine that this particular mapped type will only ever be over an array, and hence should be able to deduce that Type[K] will only ever be Type[number] (or Type[`${ number }`] for tuples).
The fact tuple keys aren't subtypes of number is explained, however that is a distinct issue. Here the problem is that K will only be the "numeric like" types (although note from that issue that for tuples, K is never assignable to number, they are exposed as string types only).
For you example, a workaround is replace Local<Type[K]> with Local<Type[K & number]>
No, that doesn't work, it causes the final type in the example to become:
Which is not correct, the existing [Local<I32>, Local<Externref>] is correct already actually, it's just that it warns in the mapped type despite the fact it produced the correct type anyway.
You are right. Local<Type[K & number]> doesn't work.
Since what we need is only 'numeric like' types in keyof T, I believe it is necessary to introduce a generic type KeyOf<T> to provide only numeric index types for array/tuple, which should be in a form of 0 | 1 | 2 ... | N-1 when T is a N-length tuple, and should be number when T is an unknown-length array.
It could be written as below.
type_GetKeysFromLength<Lextendsnumber,Rextendsreadonlyunknown[]=[],>=numberextendsL ? number : R['length']extendsL ? R[number] : _GetKeysFromLength<L,readonly[R['length'], ...R]>;typeKeyOf<Textendsunknown[]>=_GetKeysFromLength<T['length']>extends infer Uextendsnumber ? U : never;// for versions prior to v4.7// type KeyOf<T extends unknown[]> = _GetKeysFromLength<T['length']> extends infer U ? U extends number ? U : never : never;typeR1=KeyOf<[1,2,3]>;// ^? type R1 = 0 | 2 | 1typeR2=KeyOf<number[]>;// ^? type R2 = numbertypeLocalsFor<TypeextendsArray<ValType>>={[KinKeyOf<Type>]: Local<Type[K]>};constlocals: LocalsFor<[I32,Externref]>=[newLocal(newI32()),newLocal(newExternref()),];// seems OK now!varl0=locals[0]// ^? var l0: Local<I32>varl1=locals[1]// ^? var l1: Local<Externref>
In this particular case, we have an explicit Type extends ReadonlyArray so TypeScript should be able to deduce just fine that this particular mapped type will only ever be over an array, and hence should be able to deduce that...
This is the incorrect assumption.
Matching the constraint extends ReadonlyArray<ValType> does not provide a bound on an arbitrary Type[keyof Type], as shown:
typeM=ReadonlyArray<ValType>&{k: boolean};typeLocalsFor<TypeextendsReadonlyArray<ValType>>={// Type 'Type[K]' does not satisfy the constraint 'ValType'.// Type 'Type[keyof Type]' is not assignable to type 'ValType'.// Type 'Type[string] | Type[number] | Type[symbol]' is not assignable to type 'ValType'.// Type 'Type[string]' is not assignable to type 'ValType'.(2344)[KinkeyofType]: Local<Type[K]>};typeG=LocalsFor<M>;typeX=G["k"];// x: Local<boolean>, a constraint violation
Matching the constraint extends ReadonlyArray<ValType> does not provide a bound on an arbitrary Type[keyof Type], as shown:
These bizarre rules make the whole point of "homomorphic mapped types" seem fairly weak and unreliable, like if there's gonna be special casing for arrays in mapped types to not map stuff like methods, why would we want it to map other random properties that might be attached onto the array? (as an example, symbol-keyed protocols like Symbol.for("nodejs.util.inspect.custom") and such, people probably wouldn't want such properties suddenly appear in mapped types if no other methods do)
The amount of bizarre behaviours in the mapped types feature just constantly surprises me, I don't see how any developers who aren't intimately familiar with TypeScript's internals are expected to have any reasoning about these complicated features (and it doesn't help that that handbook doesn't even mention arrays are (kind've) homomorphic in mapped types to begin with, this was something just mentioned once when the feature was released in the associated release blog post).
Activity
whzx5byb commentedon Apr 17, 2022
I think it is explained in #48398.
For you example, a workaround is replaceLocal<Type[K]>withLocal<Type[K & number]>Jamesernator commentedon Apr 17, 2022
The fact tuple keys aren't subtypes of
numberis explained, however that is a distinct issue. Here the problem is thatKwill only be the "numeric like" types (although note from that issue that for tuples,Kis never assignable tonumber, they are exposed as string types only).No, that doesn't work, it causes the final type in the example to become:
Which is not correct, the existing
[Local<I32>, Local<Externref>]is correct already actually, it's just that it warns in the mapped type despite the fact it produced the correct type anyway.whzx5byb commentedon Apr 17, 2022
You are right.
Local<Type[K & number]>doesn't work.Since what we need is only 'numeric like' types in
keyof T, I believe it is necessary to introduce a generic typeKeyOf<T>to provide only numeric index types for array/tuple, which should be in a form of0 | 1 | 2 ... | N-1whenTis a N-length tuple, and should benumberwhenTis an unknown-length array.It could be written as below.
Playground
RyanCavanaugh commentedon Apr 18, 2022
This is a correct error.
This is the incorrect assumption.
Matching the constraint
extends ReadonlyArray<ValType>does not provide a bound on an arbitraryType[keyof Type], as shown:Jamesernator commentedon Apr 18, 2022
These bizarre rules make the whole point of "homomorphic mapped types" seem fairly weak and unreliable, like if there's gonna be special casing for arrays in mapped types to not map stuff like methods, why would we want it to map other random properties that might be attached onto the array? (as an example, symbol-keyed protocols like
Symbol.for("nodejs.util.inspect.custom")and such, people probably wouldn't want such properties suddenly appear in mapped types if no other methods do)The amount of bizarre behaviours in the mapped types feature just constantly surprises me, I don't see how any developers who aren't intimately familiar with TypeScript's internals are expected to have any reasoning about these complicated features (and it doesn't help that that handbook doesn't even mention arrays are (kind've) homomorphic in mapped types to begin with, this was something just mentioned once when the feature was released in the associated release blog post).
Jamesernator commentedon Apr 18, 2022
Either way, I think I'll reopen the other issue because that one at the very least checks the keys for "numeric"-ness.
RyanCavanaugh commentedon Apr 18, 2022
Every single rule we've added to mapped types was put there to remove some other surprise π€·ββοΈ
jcalz commentedon Apr 20, 2022
So is #27995 not really a bug then?
jcalz commentedon Apr 27, 2022
This has changed from "working as intended" to "fixed" by #48837. Not sure if anyone wants to alter tags here.