Repository navigation
number should be subtype of keyof T[] #13715
Description
Activity
- addedDesign LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixed
on Jan 27, 2017 At runtime, any index is really a string. so
keyof Tis a subtype ofstringand notstring|number. If you are indexing into an array, i would just usenumber.The following example is mentioned in
keyofand Lookup Types:function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; // Inferred type is T[K] }
It does not work to access elements of
TifTis an array:getProperty(array, 1) // Type '1' is not assignable to type '"length" | "toString" | "toLocaleString" | "push" | "pop" | "concat" | "join" | "reverse" | "shif...'.
You can solve this by overloading:
function getProperty<E>(obj: E[], key: number): E function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] function getProperty(obj: any, key: any): any { return obj[key] }
So I just did what you suggested:
If you are indexing into an array, i would just use
number.However, this does not work, if
Tis a type variable of an interface:interface Wrapper<T> { getProperty<K extends keyof T>(key: K): Wrapper<T[K]> }
If
Tis not an array thennumbershould not be allowed askey(which is the case in the example above). However, ifTis an array you can not add an overloaded signature like before, because that invalidates the case of the previous sentence (Tis not an array). Besides that, you don't know the element type ofT.interface Wrapper<T> { // the overload in the next line should only be applied if T is an array getProperty(key: number): any // return type is any since you do not know the element type of T getProperty<K extends keyof T>(key: K): Wrapper<T[K]> }
If
numberwould be subtype ofkeyof TandTis an arrayE[]thenT[number]would returnE. IfTis not an array thennumberis not subtype ofkeyof T.Reacted by Lenny Lixalot and Elmer BulthuisMoreover, it seems inconsequent to me that
keyof { [key: string]: number }isstringandkeyof { [key: number]: number }isnever.Reacted by interphx, Elmer Bulthuis and Alberto AldegheriWe just hit this limitation ourselves. We're filtering some command objects that have their type identified by a numeric ID and were hoping to have them properly typed in our filter method. This is the workaround we have right now, but as you can see it's ugly. We're passing the ID in as a string instead of a number (yuck) and then converting every command's ID to string to compare it, also yuck.
If numbers were supported in keyof as well as strings, this would just work.
interface Commands { [id: number]: IncomingCommand; 20: IncomingCommand & { commandTwentySpecificValue: number } } function on<TId extends keyof Commands>(id: TId, f: (command: Commands[TId]) => void){ // Calls f for every command that passes through where potentialCommand.id.toString() === id } on("20", command => { console.log(command.commandTwentySpecificValue); });
DanielRosenwasser commented
on Mar 31, 2017 MemberMore actionsWhat about adding an overload like this?
function getProperty<T>(obj: { [index: number]: T }, key: number): T
It wouldn't fix all your problems, but it'd work in certain places.
Faced with this when tried to improve immutablejs methods with typesafety.
interface Record<T> { setIn<K1 extends keyof T>(k: [K1], v: T[K1]): void; setIn<K1 extends keyof T, K2 extends keyof T[K1]>(k: [K1, K2], v: T[K1][K2]): void; setIn<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]>(k: [K1, K2, K3], v: T[K1][K2][K3]): void; setIn<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], K4 extends keyof T[K1][K2][K3]>(k: [K1, K2, K3, K4], v: T[K1][K2][K3][K4]): void; setIn<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], K4 extends keyof T[K1][K2][K3], K5 extends keyof T[K1][K2][K3][K4]>(k: [K1, K2, K3, K4, K5], v: T[K1][K2][K3][K4][K5]): void; }
This works nicely until T is nested object. When it'd be Array or Map - keyof can't detect keytype of it.
Maybe solution could be ability to explicitly declare keyof. E.g.
type MyArray = { [keyof]: number }
Also saw you are planning making variadic generics. Will they help solving this problem?
Reacted by Elmer BulthuisWhy was this closed? Should we take it you never intend to support this feature? Does this count as wont-fix or working-as-intended?
The issue was auto-closed since there was no clear action. and has not been touched in more than 14 days.
As noted earlier, number does not exist at runtime, only strings. a solution would be to add a new primitive type
numericStringthat is a subtype ofstringbut is comparable tonumber, then change all these index signatures to allow for use ofnumericStringand convertnumberimplicitly tonumericString. This however seems too complex both from implementation and from usage perspective compared to the value gained by addressing the issue.The issue was auto-closed since there was no clear action. and has not been touched in more than 14 days.
Mohamed Hegazy (@mhegazy) Please reopen. In my opinion, this discussion is not done. I guess Vadym Holoveichuk (@goloveychuk) and Lenny Lixalot (@LennyLixalot) do agree. No one reacted to my counter arguments:
They are approved by use cases of Vadym Holoveichuk (@goloveychuk) and Lenny Lixalot (@LennyLixalot). @goloveychuk's use case refers to the current design limitation regarding generic type variables of an object and @LennyLixalot's use case refers to the mentioned design discrepancy. My suggestion would solve both use cases.
The only argument for the current design decision is
At runtime, any index is really a string. so
keyof Tis a subtype ofstringand notstring|number. If you are indexing into an array, i would just usenumber.I still don't get, why at runtime is a design factor in this case.
The 2 in years[2] is coerced into a string by the JavaScript engine through an implicit toString conversion.
See MDNIf at runtime is crucial, indexing into an array using
numberwouldn't be allowed. That wouldn't be useful and TypeScript allowsnumberas key of arrays. But wait!numberis no subtype ofkeyof string[]numberis notkeyof Container<T>even though it is explicitly declared as such:interface Container<T> { [index: number]: T }
Mohamed Hegazy (@mhegazy) Please elaborate on this. Why should at runtime still be a design factor in this case, but not in others?
Reacted by Kurt Preston, Lenny Lixalot and Elmer BulthuisYes, we agree that something should be done about this, it's overly restrictive and has forced us to make a few sub-par design decisions to maintain strong-typing.
This how the feature was implemented originally, and we ran into other issues and had to be restricted to a subtype of string. see more details in #12425.
So unless there is a new proposal on how to mitigate these issues but still support indexing with numbers, not sure reopening this issue adds much.
Mohamed Hegazy (@mhegazy) where are those other issues documented/mentioned? Do you mean #12314? Are there other issues?
Knowing those issues helps to find a proper solution 🙂
Reacted by Marin MarinovAs far as I've seen, all those issues are related to instantiated types, that can not be typed using
keyofregarding to @ahejlsberg. That includes:Object.keysObject.entriesfor..in
They are no valid use case of
keyof. It seems to me thatkeyofis widely misunderstood.What
keyofis notGiven an object
objof actual typeOand an interfaceIwithO extends I, implieskeyof O extends keyof I. You never absolutely knowOin TypeScript. You only know an interface likeI. That means you never absolutely knowkeyof O. You only knowkeyof I.Object.keys,Object.entriesandfor..inall usekeyof O, which you do not know.Note: You may argue that
Ocan not contain keys of typenumbersince they are actuallystrings at runtime. However, the only possible way to access a property of an object isobj[key]andkeyis coerced tostring. So ifOaccepts all keys of typenumericString(like arrays or a lookup type{ [key: number]: P }) you can safely passnumbers askeytoobj[key]That means, the interfaceOcan containnumberas keys even though the actual typeOdoes not contain them. The same applies to number literals. IfOhas key"0"it accepts key0.Summarized,
keyofdoes not absolutely describe all keys of an object at runtime.What
keyofis or what it should bekeyofdescribes keys of an interface. This interface must not describe everything of an object at runtime. It may only contain a part of it. Further, it should contain keys of typenumberas stated in the note of the previous section. However, the issue with that is the following:Given
interface ObjWithKey42<T> { 42: T }
and
objof typeObjWithKey42<T>, the expressionsobj[42]andobj['42']are of typeT. As a consequence,keyof ObjWithKey42<T>is42 | '42'. This works for multiplenumberkeys as well:interface ObjWithKey1To3<T1, T2, T3> { 1: T1 2: T2 3: T3 }
keyof ObjWithKey1To3<T1, T2, T3>is1 | '1' | 2 | '2' | 3 | '3'. So far, so good. However, we get into trouble with lookup types:interface Container<T> { [index: number]: T }
We can not list all these numeric string literals (effectively).
keyof Container<T>would benumber | numericStringwithnumericStringas type of all numeric string literals with a corresponding number literal.number | stringwouldn't be correct because not everystringis anumericString. Also not every string sequens of digit characters is anumbericStringsincenumberhas a maximum and minimum value.Mohamed Hegazy (@mhegazy) I guess, that's what you are trying to say.
Reacted by Elmer BulthuisI think so :).
keyof Tis always subtype ofstring. How about something similar:indexof Tis always subtype ofnumber:interface Thing { name: string; width: number; height: number; inStock: boolean; } type K1 = keyof Thing; // "name" | "width" | "height" | "inStock" type I1 = indexof Thing; // never type K2 = keyof Thing[]; // "length" | "push" | "pop" | "concat" | ... type I2 = indexof Thing[]; // number type K3 = keyof { [x: string]: Thing }; // string type I3 = indexof { [x: string]: Thing }; // number type K4 = keyof { [x: number]: Thing }; // never type I4 = indexof { [x: number]: Thing }; // number type K5 = keyof { 0: Thing, "1": Thing }; // "0" | "1" type I5 = indexof { 0: Thing, "1": Thing }; // 0 | 1
Reacted by Kurt Preston, swandir, Robert R., interphx, Ryoji Miyazato, Yarn, Tom Yaxley, Elmer Bulthuis and Lucasindexofcombined withkeyofwould solve the generic issue:interface Wrapper<T> { getProperty<K extends (keyof T | indexof T)>(key: K): Wrapper<T[K]> }
Reacted by Elmer BulthuisMohamed Hegazy (@mhegazy) hi, is there any progress?
I also faced same problem.
In JavaScript library, mentioned interface such agetPropertyis commonly exists. ex: The best popular librarylodashhas same interface at#get.
So I really hope to support yielding number type atkeyof T[].- added a commit that references this issue
on Apr 20, 2018 - addedFixedA PR has been merged for this issueA PR has been merged for this issue
on Apr 20, 2018 - removedDesign LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixed
on Apr 23, 2018 - locked and limited conversation to collaborators
on Jul 31, 2018
TypeScript Version: 2.1.5
Code
Expected behavior:
numbershould be subtype ofkeyof string[]andkeyof Container<string>Actual behavior:
numberis no subtype ofkeyof string[]andkeyof Container<string>