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
Numeric template type can't be used to index tuple in mapped type (despite actually working anyway) #48739
This is the behavior in every version I tried, and I reviewed the FAQ for entries about template type
Note
This is a followup to another issue I raised where similar issues arose, however here the exact cause is different and more obviously a bug rather than intended behaviour.
This might still be a design limitation, but it's unclear to me how, without this, one can map a tuple type to something with a generic constraint.
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>>={[KinkeyofType]: Kextendsnumber ? Local<Type[K]>// Type 'Type[K]' does not satisfy the constraint 'ValType'.// Type 'Type[`${number}` & string]' is not assignable to type 'ValType'.(2344)
: Kextends `${ number}` ? Local<Type[K]>
: Type[K]};// This is still the correct type, despite the above error, so clearly// the keys "0" and "1" are successfully interpreted as `${ number }` in// the conditional typeconstlocals: LocalsFor<[I32,Externref]>=[newLocal(newI32()),newLocal(newExternref()),];
🙁 Actual behavior
It produces an error, despite the fact K extends `${ number }` should be treated as a tuple key. (As per the other issue I raised numbers can't be used to index tuples, so a numeric string is the only way to distinguish tuple entry keys from methods for mapping tuple types).
The final bit of code, shows that the constraint does work, however it's not treated as a valid key for ReadonlyArray<ValType> despite subtypes being potentially tuples.
Actually I might close this and open an issue on a more basic failure in mapped types, the fact that constraints don't work despite mapped types ignoring methods in array/tuple types:
Adding more ad-hoc rules around e.g. K extends ${number} is just going to make mapped types more "bizarre", per your comment in the other thread, so I'm not sure what avenue to pursue here except for using the above definition.
The problem with that is it deletes all the other properties (and without negated types, we can't do [K in keyof Type & not number] to merge in the rest):
constlocals: LocalsFor<[I32,Externref]>=[newLocal(newI32()),newLocal(newExternref()),];// Property 'length' does not exist on type 'LocalsFor<[I32, Externref]>'.console.log(locals.length);
Adding more ad-hoc rules around e.g. K extends ${number} is just going to make mapped types more "bizarre"
This wouldn't really be ad-hoc, like "0" is clearly ${ number }, this is just true. What's weirder is that [K in number & keyof Type] works, given that usually K extends number is false?:
Like why would number & keyof Type be expected to be something other than never if for tuples keyof doesn't return the keys as number to begin with? Except that it actually magically does in this very specific situation? (Which again isn't documented in the handbook, so how is anyone supposed to figure out that would work, given that the logical combination of parts suggests it wouldn't?)
Also just to be clear, the example in the initial comment almost works, like the type is mapped just fine (as LocalsFor<[I32, Externref]> DOES correctly map the "0" and "1" keys through the condition K extends `${ number }`). However what is incorrect is that TypeScript doesn't consider Type[`${ number }`] to be ValType DESPITE the fact it does consider Type[number] (as in the first conditional).
As you've pointed out in the previous issue, the keys aren't exposed as numbers, because well they aren't numbers. However neither are Array keys, so Type[`${ number }`] should work when indexing the array Type (and again, this technically already works for tuple keys).
declare global {interfaceReadonlyArray<T>{[k: `${ number}`]: T;}}
This should probably still be in the standard types that TypeScript ships as this is, well, how arrays in JS work. But at least I have an actually good workaround now.
No, because 0.2 is a valid ${number} but not an array index.
I mean given that TypeScript already behaves like this for the arrays:
constarr=[1,2,3];// x is type numberconstx=arr[0.2]
It wouldn't be that inconsistent. If people really want to attach non(positive)-integer numbers to arrays, they already do so at their own peril today anyway.
Like if TypeScript ever fixed array[number] to be array[positiveInteger], then obviously the string indexer would be array[`${positiveInteger}`]. But from what I've seen TS has already considered int types out of scope. So I don't see why TypeScript would care that ReadonlyArray[`${ number }`] is technically wrong when ReadonlyArray[number] already is.
WLOG (constrained to your scenario where presumably you don't care about that example, which is super valid) you can write
This one suffers the problem of deleting types again, although even if it's changed to:
It just breaks the type in ways I'm not entirely clear on, and I'm not sure what a minimal reproduction actually is:
I suspect it has to do with the extra indirection through more generics, my suggested fix doesn't suffer this problem though and instead there is no type error there, so the actual array parts are preserved properly with my fix.
or
This suffers exactly the same problem as the previous suggestion.
I'm not clear why this is the case, adding the signature I suggested just above would basically be consistent with how TypeScript already treats array[number] today.
I'm not clear why this is the case, adding the signature I suggested just above would basically be consistent with how TypeScript already treats array[number] today.
The cure has to be better than the disease. Adding that declaration would permit things like
I'm calling this unactionable because not liking how mapped type are/aren't homomorhpic or do/don't deal with numeric-like keys isn't something we can change in a way that doesn't cause someone else to not like it instead, with the added drawback of being a breaking change.
Why were int types rejected as a suggestion then? Because if array[number] is considered a "disease", then that was/is the obvious solution. (And then array[`${int}`] or whatever could be added safely because that would be correct).
This is something that has been frustrating me with TypeScript for quite a long time now, and that's that there is a lot of historical baggage that has fixes outright rejected (like the aforementioned int-types) or have been suggested for years with no indication of fixing either way (there are way too many of these I've encountered to even begin listing).
It's a shame because there really just aren't major competitors to TypeScript (Flow doesn't really compete in the same space, as it's more a subset of JS that is strictly typed, rather than adding types to JS, and they announced a fairly significant direction change recently to non-standard JS features). There is one ridiculously excellent type system in Hegel, however it lacks one main feature which is why I've stayed with TypeScript and that's suggestions/completions.
By the way... using & number in the key part of the mapped type is somewhat suboptimal because such a type won't be usable with spreads: TS playground
The problem here is that the result of this produces an object type with an index signature. If we really want this then there is no point in using a mapped type here at all because we will end up with a single key~ anyway. This could be rewritten to:
So it seems that a better approach is to actually use & number in the template part of the mapped type: TS playground
That on the other hand... works somewhat accidentally. The problem in here is that when mapping over a tuple we'll get keys like '0' | '1' | '2' | .... So why does it even work? After all string & never is... never. It works because never can be used to access an index signature. TS playground demonstrating that.
The problem is... this still doesn't really work as expected. Because we've accessed an index signature we got a union back of all possible value types from this tuple and most likely the intention was to map each position separately. It is possible to type this index by index by for what seems to be a pretty standard operation... it's pretty complex and requires using nested conditional types or the new infer extends combo: TS playground
I'm also discussing this problem in the comments here: #48599
Activity
Jamesernator commentedon Apr 17, 2022
Actually I might close this and open an issue on a more basic failure in mapped types, the fact that constraints don't work despite mapped types ignoring methods in array/tuple types:
Jamesernator commentedon Apr 18, 2022
As the other issue is supposedly a working as intended, I'm gonna reopen this one.
RyanCavanaugh commentedon Apr 19, 2022
I guess what I'm not understanding is why you don't write this type
which seems to do what you're looking for?
Adding more ad-hoc rules around e.g.
K extends ${number}is just going to make mapped types more "bizarre", per your comment in the other thread, so I'm not sure what avenue to pursue here except for using the above definition.Jamesernator commentedon Apr 19, 2022
The problem with that is it deletes all the other properties (and without negated types, we can't do
[K in keyof Type & not number]to merge in the rest):This wouldn't really be ad-hoc, like
"0"is clearly${ number }, this is just true. What's weirder is that[K in number & keyof Type]works, given that usuallyK extends numberis false?:Like why would
number & keyof Typebe expected to be something other thanneverif for tupleskeyofdoesn't return the keys asnumberto begin with? Except that it actually magically does in this very specific situation? (Which again isn't documented in the handbook, so how is anyone supposed to figure out that would work, given that the logical combination of parts suggests it wouldn't?)Jamesernator commentedon Apr 19, 2022
Also just to be clear, the example in the initial comment almost works, like the type is mapped just fine (as
LocalsFor<[I32, Externref]>DOES correctly map the"0"and"1"keys through the conditionK extends `${ number }`). However what is incorrect is that TypeScript doesn't considerType[`${ number }`]to beValTypeDESPITE the fact it does considerType[number](as in the first conditional).As you've pointed out in the previous issue, the keys aren't exposed as numbers, because well they aren't numbers. However neither are
Arraykeys, soType[`${ number }`]should work when indexing the arrayType(and again, this technically already works for tuple keys).Jamesernator commentedon Apr 19, 2022
Okay so funnily enough, just adding the signature myself actually works:
This should probably still be in the standard types that TypeScript ships as this is, well, how arrays in JS work. But at least I have an actually good workaround now.
RyanCavanaugh commentedon Apr 19, 2022
No, because
0.2is a valid${number}but not an array index.We're still left with the problem identified in the other issue
WLOG (constrained to your scenario where presumably you don't care about that example, which is super valid) you can write
or
depending on what you want to happen to out-of-band values.
Jamesernator commentedon Apr 19, 2022
I mean given that TypeScript already behaves like this for the arrays:
It wouldn't be that inconsistent. If people really want to attach non(positive)-integer numbers to arrays, they already do so at their own peril today anyway.
Like if TypeScript ever fixed
array[number]to bearray[positiveInteger], then obviously the string indexer would bearray[`${positiveInteger}`]. But from what I've seen TS has already considered int types out of scope. So I don't see why TypeScript would care thatReadonlyArray[`${ number }`]is technically wrong whenReadonlyArray[number]already is.This one suffers the problem of deleting types again, although even if it's changed to:
It just breaks the type in ways I'm not entirely clear on, and I'm not sure what a minimal reproduction actually is:
I suspect it has to do with the extra indirection through more generics, my suggested fix doesn't suffer this problem though and instead there is no type error there, so the actual array parts are preserved properly with my fix.
This suffers exactly the same problem as the previous suggestion.
I'm not clear why this is the case, adding the signature I suggested just above would basically be consistent with how TypeScript already treats
array[number]today.RyanCavanaugh commentedon Apr 19, 2022
The cure has to be better than the disease. Adding that declaration would permit things like
I'm calling this unactionable because not liking how mapped type are/aren't homomorhpic or do/don't deal with numeric-like keys isn't something we can change in a way that doesn't cause someone else to not like it instead, with the added drawback of being a breaking change.
Jamesernator commentedon Apr 19, 2022
Why were int types rejected as a suggestion then? Because if
array[number]is considered a "disease", then that was/is the obvious solution. (And thenarray[`${int}`]or whatever could be added safely because that would be correct).This is something that has been frustrating me with TypeScript for quite a long time now, and that's that there is a lot of historical baggage that has fixes outright rejected (like the aforementioned
int-types) or have been suggested for years with no indication of fixing either way (there are way too many of these I've encountered to even begin listing).It's a shame because there really just aren't major competitors to TypeScript (Flow doesn't really compete in the same space, as it's more a subset of JS that is strictly typed, rather than adding types to JS, and they announced a fairly significant direction change recently to non-standard JS features). There is one ridiculously excellent type system in Hegel, however it lacks one main feature which is why I've stayed with TypeScript and that's suggestions/completions.
Andarist commentedon Apr 22, 2022
By the way... using
& numberin the key part of the mapped type is somewhat suboptimal because such a type won't be usable with spreads:TS playground
The problem here is that the result of this produces an object type with an index signature. If we really want this then there is no point in using a mapped type here at all because we will end up with a single key~ anyway. This could be rewritten to:
TS playground with the code above
So it seems that a better approach is to actually use
& numberin the template part of the mapped type:TS playground
That on the other hand... works somewhat accidentally. The problem in here is that when mapping over a tuple we'll get keys like
'0' | '1' | '2' | .... So why does it even work? After allstring & neveris...never. It works becausenevercan be used to access an index signature. TS playground demonstrating that.The problem is... this still doesn't really work as expected. Because we've accessed an index signature we got a union back of all possible value types from this tuple and most likely the intention was to map each position separately. It is possible to type this index by index by for what seems to be a pretty standard operation... it's pretty complex and requires using nested conditional types or the new
infer extendscombo:TS playground
I'm also discussing this problem in the comments here: #48599
Andarist commentedon Apr 25, 2022
James Browning (@Jamesernator) Ryan Cavanaugh (@RyanCavanaugh) I believe that the issue here was addressed by #48837
github-actions commentedon Jun 8, 2023
This issue has been marked as 'Unactionable' and has seen no recent activity. It has been automatically closed for house-keeping purposes.