Repository navigation
Mapped tuples types iterates over all properties #27995
Description
Activity
This is because, despite mapped types not behaving as such,
keyof ["some", "tuple"]still returns0 | 1 | "length" | ... | "whatever"instead of0 | 1.Reacted by Mathieu Lemoine, Maciej Sikora, csr632, Brandon Bloom, Benjamin Kroeger, lordlycastle, okmttdhr, tada, ExE Boss, Sean Blonien, Victor Garcia and 3 more- addedBugA bug in TypeScriptA bug in TypeScriptDomain: Mapped TypesThe issue relates to mapped typesThe issue relates to mapped types
on Oct 19, 2018 I wouldnt mind a fix for this as well. I've been encountering similar issues, but here's how I've been working around it for anyone else with this problem: (my code generalized below with Boxes)
interface Box<T> { value: T; } type UnBox<T extends Box<unknown>> = T extends Box<infer U> ? U : never; type UnBoxTuple<T extends Box<unknown>[]> = { [P in keyof T]: UnBox<T[P]>; };
Above code complains that
T[P]does not satisfy the constraintBox<unknown>. My current fix has been just to manually narrow the type with a conditional like so: (Edit: looks like this format is required for using mapped tuples like this, #27351)type UnBoxTuple<T extends Box<unknown>[]> = { [P in keyof T]: T[P] extends T[number] ? UnBox<T[P]> : never; };
Trying to apply the same to Mathieu Lemoine (@lemoinem)'s example didn't work at first. It seems like the mapped tuple special-case only applies to generics (try the following in ts 3.1.3 to see what I mean):
type Foo = ["a", "b"]; type GenericTupleIdentity<T extends unknown[]> = { [K in keyof T]: T[K] }; type FooIdentity = { [K in keyof Foo]: Foo[K] } type GenericTupleIdentityTest = GenericTupleIdentity<Foo> // ["a", "b"] type FooIdentityTest = FooIdentity // a Foo-like object no longer recognised as a tuple
Not sure if this inconsistency is intended or not. Abstracting-out
FooandBarand adding a manual type-narrower gets us:type Bazify<B, F extends (keyof B)[]> = { [K in keyof F]: F[K] extends F[number] ? B[F[K]] : never; };
Which can then be called with
Bazify<Bar, Foo>(or usetype Baz = Bazify<Bar, Foo>;to keep existing-code-changes to a minimum)It's worth noting that
T[P] extends T[number]is used instead of simplyP extends numberbecause for some reason P acts like a pre 2.9 key and always extendsstring("0" | "1" | "length" | ...) as opposed tostring | number | symbol(0 | 1 | "length" | ...), which is probably a separate issue of its own.Reacted by Mathieu Lemoine and Evžen ZelinkaReacted by Edward Kotarski, EllaSharakanski and Francois ChabotReacted by ExE BossThe issue here is that we only map to tuple and array types when we instantiate a generic homomorphic mapped type for a tuple or array (see #26063). We should probably also do it for homomorphic mapped types with a
keyof TwhereTis non-generic type.Reacted by Mathieu Lemoine, Titian Cernicova-Dragomir, Maximilian Mairinger, okmttdhr, tada, ExE Boss, Malcolm and AnthonyPaulOJust stumbled on what I assume is the same issue:
type n1 = [1, 2, 3] type n2t<T> = {[k in keyof T]: 4} type n2 = { [k in keyof n1]: 4 } // { length: 4, find: 4, toString: 4, ...} type n2b = n2t<n1> // [4, 4, 4] as expectedI would expect n2 to behave like n2b.
Reacted by Malcolm, luo jiyin and Chong Jia WeiReacted by Chase MoskalThis also means that these constructs don't work:
type Test1<S extends number[]> = Record<number, "ok">[S[keyof S]]; type Test2<S extends [0, 1, 2]> = Record<number, "ok">[S[keyof S]];The second one should definitely work. This means that it's impossible to map tuples to other tuples with a method that requires the value type of the input tuple to be known to extend something (e.g. number as above)
Mathieu Lemoine (@lemoinem) You can get your current example to narrow correctly and avoid the behavior Wesley Wigham (@weswigham) mentioned with a couple of helper conditional types. Note: this comes with some not so great drawbacks.. You lose all array prototype methods and properties, which for my current use case--messing with React hooks--that tradeoff isn't the worst thing in the world.
export type ArrayKeys = keyof any[]; export type Indices<T> = Exclude<keyof T, ArrayKeys>; type Foo = ['a', 'b']; interface Bar { a: string; b: number; } type Baz = { [K in Indices<Foo>]: Bar[Foo[K]] }; // type is { "0": string, "1": number }
The way this type ends up formatting isn't the greatest, but it ultimately maps to some of your desired outcomes. I am having issues around some constructs I thought would work, similar to phiresky 's examples. I will get some better examples together and document those, but for now just wanted to provide a bit of a workaround.
Reacted by jituanlin, Ron Spickenagel, John, Aleksey Levenstein, Maciej Sikora, Mingwei Samuel, XueFeng, Eliot Lash, Arnau Sanchez Sala, Ondratra and 1 moreReacted by Aleksandras AndrejevasKevin Saldaña (@ksaldana1) Thanks, nice idea!
Actually my ownWorkingBazis a pretty good workaround as well, if I can say so myself:type WorkingBaz = { [K in Exclude<keyof Foo, keyof any[]>]: Bar[Foo[K]]; } & { length: Foo['length']; } & any[]
It avoids the drawback of losing the Array prototype's methods (only their typing info, which are already much less useful on heterogeneous tuples...).
Although it's not recognized as[string, number], as far as I can tell, it's still an almost structurally equivalent type:
{ "0": string; "1": number; } & { length: 2; } & any[].But I'd still expect this to work out of the box and produce the correct
[string, number]type.Reacted by Kevin Saldaña, Itamar Schen, bradleat, Bnaya Peretz, Robert Monfera and Carlos Fdez. LlamasReacted by Itamar Schen and Robert MonferaI'm also seeing the same bug. I was about to log an issue, but I saw this one in the related issues page.
For reference, the code I'm experiencing this bug with:
Search Terms:
tuple mapped type not assignable
Codetype Refinement<A, B extends A> = (a: A) => a is B // we get the error `Bs[k] does not satisfy the constraint 'A'. Type A[][k] is not assignable to type A` type OrBroken = <A, Bs extends A[]>(...preds: { [k in keyof Bs]: Refinement<A, Bs[k]> }) => Refinement<A, Bs[number]> // same (well similar, [A, A, A][k] is not assignable vs A[][k] not assignable) error type OrBroken1 = <A, Bs extends [A, A, A]>(...preds: { [k in keyof Bs]: Refinement<A, Bs[k]> }) => Refinement<A, Bs[number]> // if we don't map over the type we don't receive the error type OrWorks = <A, Bs extends A[]>(...preds: { [k in keyof Bs]: Refinement<A, Bs[0]> }) => Refinement<A, Bs[number]> type OrWorks1 = <A, Bs extends A[]>(...preds: { [k in keyof Bs]: Refinement<A, Bs[number]> }) => Refinement<A, Bs[number]>
Expected behavior:
When mapping over a tuple typeBswhere every element extends some typeA, the mapped typeBs[k]should be assignable toA. In other words, OrBroken in the above example should not give an error
Actual behavior:
We recieve the errorBs[k] does not satisfy the constraint 'A'. Type A[][k] is not assignable to type APlayground Link:
linkYou could just filter for number properties:
type Foo = ['a', 'b']; interface Bar { a: string; b: number; } // Inverse of Exclude<> type FilterOnly<T, N> = T extends N ? T : never; type Baz = { [K in FilterOnly<keyof Foo, number>]: Bar[Foo[K]]; };
Reacted by Bnaya Peretz, lonewarrior556, James Abbott and Shravan SunderReacted by Denis Mukhametov and Aleksandras AndrejevasI think we have enough and various workarounds and comments from the TS team identifying the root issue.
If you have the same issue, may I suggest you add a thumbs up/+1 reaction to the initial comment (and subscribe to the issue so you will know when this is finally fixed) instead of commenting "Me too".This will prevent spamming everyone in the thread. Thank you very much.
(I'm still eagerly waiting for a fix to this ;) )Reacted by Ingo BürkHey there, I'm running into this issue, except I'm using a generic tuple type instead of a statically declared tuple.
type PropertyType = "string" | "number" | "boolean" type PropertyValueTypeMap = { string: string, number: number, boolean: boolean } type Args<A extends Array<PropertyType>> = { // ERROR: Type 'A[I]' cannot be used to index type 'PropertyValueTypeMap'. [I in keyof A]: PropertyValueTypeMap[A[I]] } // RESULT: type A = [string, number] type A = Args<["string", "number"]>
Augusto Moura (@augustobmoura)'s
FilterOnlyapproach doesn't work.
Kevin Saldaña (@ksaldana1)'sExclude<keyof Foo, keyof any[]>approach also does not work.Reacted by Aleksandras AndrejevasConsidering that this works:
type Args<A extends Array<PropertyType>> = { 0: PropertyValueTypeMap[A[0]] 1: PropertyValueTypeMap[A[1]] 2: PropertyValueTypeMap[A[2]] }
... I would have expected this to work, but it doesn't:
type Args<A extends Array<PropertyType>> = { [K in keyof A]: K extends number ? PropertyValueTypeMap[A[K]] : never }
Also tried this...
type Args<A extends Array<PropertyType>> = { [K in keyof A]: K extends keyof Array<any> ? never : PropertyValueTypeMap[A[K]] }
Reacted by dave kraftsow, Bnaya Peretz, Tao Cumplido and keatkeat8710 remaining items
Anders Hejlsberg (@ahejlsberg)
We should probably also do it for homomorphic mapped types with a
keyof TwhereTis non-generic type.And maybe also where
Tis a generic type constrained to an array/tuple type likeT extends Array<X>?Reacted by Trey BrisbaneAt the very top of the thread, Wesley Wigham (@weswigham) commented:
keyof ["some", "tuple"]still returns0 | 1 | "length" | ... | "whatever"instead of0 | 1.Did this behavior change at some point? Per this playground,
keyof [string, string]types asnumber | keyof [string, string]. Tuples should definitely type with numeric-literal keys, rather thannumber, right? Does this need its own issue, possibly as a regression?James Bromwell (@thw0rted) I don't believe they have numeric-literal keys, but rather numeric-string-literal keys. In your case, I believe
keyof [string, string]would benumber | "0" | "1".Playground example (hover over
footo see its type).OK, looks like what was confusing was that now the language service emits "keyof [string,string]" instead of the whole explicit litany of Array member functions, unless you remove one of them as in your example. Check the Asserts tab of this Bug Workbench -- it shows
type TT1 = number | keyof [string, string] type TT2 = number | "0" | "1" | "length" | "toLocaleString" | "pop" | "push" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" | "splice" | "unshift" | "indexOf" | "lastIndexOf" | ... 14 more ... | "includes"You can also open that Workbench and change around TS versions. There was a change at some point. The output above is from 4.2.3, but in 4.1.5, and 4.0.5, they both have the long spelled-out list (minus
"toString"for TT2 of course). Not sure if I'd call it a "regression", but there has been a change.I came here because I needed some way of using an object's entries, while not caring at all it is a tuple or a record.
I got stuck when I tried to define a type matching any property value of that object. It looks like this:const someValueInEntriesOf = <TSubject>(subject: TSubject): TSubject[keyof TSubject] => {...}
This is where I realized why I could not:
TSubject[keyof TSubject]would result in the inclusion of the Array prototype keys when an array was providen, while theObject.keysmethod would not.I did a little investigation and summarized things below:
Subjects
// Types type tuple = [number, number[], string, boolean[][]] type array = string[] type record = {zero: 0, one: 1, two: 2, three: 3} // Subjects const tupleSubject: tuple = [0, [1], 'foo', [[true]]] const arraySubject: array = ['zero', 'one', 'two', 'three'] const recordSubject: record = {zero: 0, one: 1, two: 2, three: 3}
Fetch subject key type
console.log(Object.keys(tupleSubject)) // logs <["0", "1", "2", "3"]>, as expected. BTW Object.keys returns <string[]> instead of <(keyof typeof arraySubject)[]>. type tupleKeyofKeys = keyof tuple // resolves as <number | keyof tuple>, why does it include <number>? And why does <keyof tuple> is made of itself anyway? type tupleRealKeys = Exclude<keyof tuple, keyof []> // resolves as <"0" | "1" | "2" | "3">, fixes the issue. console.log(Object.keys(arraySubject)) // logs <["0", "1", "2", "3"]>, as expected. BTW Object.keys returns <string[]> instead of <(keyof typeof arraySubject)[]>. type arrayKeyofKeys = keyof array // resolves as <number | keyof array>, why does it include <keyof array>, is <number> not sufficient? type arrayRealKeys = number // using <Exclude<keyof array, keyof []>> resolves as <never> :/ console.log(Object.keys(recordSubject)) // logs <["zero", "one", "two", "three"]>, as expected. BTW Object.keys returns string[] instead of <(keyof typeof recordSubject)[]>. type recordKeyofKeys = keyof record // resolves as <keyof record>, which further resolves as <"zero" | "one" | "two" | "three">, good. type recordRealKeys = Exclude<keyof record, keyof []> // resolves as <"zero" | "one" | "two" | "three">, good too.
Fetch one of the subject's items type
Fetch one of the TUPLE's items type
type tupleKeyAccess_inRange = tuple[1] // resolves as <number[]>, good type tupleKeyAccess_outOfRange = tuple[4] // throws <Tuple type 'tuple' of length '4' has no element at index '4'.ts(2493)>, good. type tupleKeyAccess_generic = tuple[number] // resolves as <string | number | number[] | boolean[][]>, but is strictly incorrect. 4 is a number and cannot index the tuple type, see tupleKeyAccess_outOfRange. type tupleKeyAccess_keyofKeys = tuple[tupleKeyofKeys] /* resolves as (sorry for your eyes): <string | number | number[] | boolean[][] | (() => IterableIterator<string | number | number[] | boolean[][]>) | (() => { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }) | ... 30 more ... | (<A, D extends number = 1>(this: A, depth?: D | undefined) => FlatArray<...>[])>. This is not expected behaviour, seems like tupleKeyofKeys includes Object.getPrototypeOf(tupleSubject) keys. */ type tupleKeyAccess_realKeys = tuple[tupleRealKeys] // resolves as <string | number | number[] | boolean[][]>, good.
Fetch one of the ARRAY's items type
type arrayKeyAccess_inRange = array[1] // resolves as <string>, as using strict mode, I expected <string | undefined>. type arrayKeyAccess_outOfRange = array[4] // resolves as <string>, as using strict mode, I expected <string | undefined>. type arrayKeyAccess_generic = array[number] // resolves as <string>, as using strict mode, I expected <string | undefined>. type arrayKeyAccess_keyofKeys = array[arrayKeyofKeys] /* resolves as (sorry for your eyes): <string | number | (() => IterableIterator<string>) | (() => { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }) | ... 30 more ... | (<A, D extends number = 1>(this: A, depth?: D | undefined) => FlatArray<...>[])> This is not expected behaviour, seems like arrayKeyofKeys includes Object.getPrototypeOf(arraySubject) keys. */ type arrayKeyAccess_realKeys = array[arrayRealKeys] // resolves as string, good
Fetch one of the RECORD's items type
type recordKeyAccess_inRange = record['one'] // resolves as <1>, good. type recordKeyAccess_outOfRange = record['four'] // throws <Property 'four' does not exist on type 'record'.ts(2339)>, good. type recordKeyAccess_generic = record[string] // throws <Type 'record' has no matching index signature for type 'string'.ts(2537)>, very good!! type recordKeyAccess_keyofKeys = record[recordKeyofKeys] // resolves as <0 | 1 | 2 | 3>. Still good there. type recordKeyAccess_realKeys = record[recordRealKeys] // resolves as <0 | 1 | 2 | 3>, good good.
What went wrong (summary)
Object access/indexing is not consistant among containers in TypeScript (mostly for historic reason I suppose, some operators/features were missing at the time), specificaly among arrays, tuples and records:
- Arrays allow out of range access, and do not include
undefinedin the item type. - Tuples prevent out of range access using raw numeric index, but allow number typed indexes, finaly allowing out of range access.
keyof arrayOrTupleincludesObject.getPrototypeOf(arrayOrTuple)whileObject.keys(arrayOrTuple)does not.
In the meantime, records:
- prevent out of range access, or have undefined included in the item type when the keys are not precisely known (
{[key: string]: itemType}) keyof recorddoes not includeObject.getPrototypeOf(record)which is consistant withObject.keys(record)
Additionaly, those are problematic too:
- There is no way to distinguish a tuple from an array.
Object.keys(object)is typedstring[], but should be the tuple equivalent of(keyof typeof object)[]when possible. Thusobject[Object.keys(object)[0]](where object is known to have at least one property) is not valid TypeScript until explicitely casted tokeyof typeof object.
What I use now until the TypeScript lang contributors fix the thing(s)
type IsTuple<T> = T extends unknown[] ? true : false // I found no way to tell apart an array from a tuple. This line must be improved as you will see later why. type TupleFixedKeyof<T> = IsTuple<T> extends true ? Exclude<keyof T, keyof []> : keyof T // THE fix type tupleKeysIExpected = TupleFixedKeyof<tuple> // resolves as <"0" | "1" | "2" | "3">, fixes tuples. type arrayKeysIDidNotExpected = TupleFixedKeyof<array> // resolves as never... Use TupleFixedKeyof wisely (only when not using arrays, only tuples and records) until IsTuple exists natively in TypeScript some way. type recordKeysIExpected = TupleFixedKeyof<record> // resolves as <keyof record> which further resolves as <"zero" | "one" | "two" | "three">, behaviour is correct for records.
- Arrays allow out of range access, and do not include
Andarist commented
on Mar 11, 2022 ContributorMore actionsI've been digging into this a little bit with the debugger and for what it is worth - I think this could be fixed if only a mapped type like this could be "resolved" (to not call it as instantiation) in
getTypeFromMappedTypeNodethat is called bycheckMappedType.If only we could recognize there that the constraint for the iteration refers to a tuple type and that mapping would be homomorphic then the assigned
links.resolvedTypewould be computed as a tuple type. This in turn would fix places likepropertiesRelatedTobecause bothsourceandtargetwould just correctly be recognized as tuples. At the moment the target (the mapped type) is not recognized there so it's roughly treated as an object type and all its properties are checked there (and not only numerical ones).I've tried to accomplish this - but I'm not sure how to best "resolve" this mapped type, I've looked through what
instantiateType,getObjectTypeInstantiationandinstantiateMappedType(as well asgetNonMissingTypeOfSymbol) but it's unfortunately over my head at the moment and I don't know how to reuse their logic or how to apply a different one based on their inner workings.Reacted by Trey Brisbane and okmttdhr, tadaIf any part of this question has to do with generic
T(as some comments mention), then that part has been fixed by #48837.The part where the type you're iterating over is non-generic remains, though.
Andarist commented
on Apr 27, 2022 ContributorMore actionsThe part where the type you're iterating over is non-generic remains, though.
The first given repro in this thread doesn't use a generic mapped type - so I would say that this issue is still sound despite the improvements made in the #48837. Luckily there is also a fix for this issue in #48433 but it still awaits the review.
Reacted by Joe CalzarettaTo expand on Pierre Tassel (@Kyasaki)'s workaround a bit: I found it is possible to distinguish between tuples and arrays like this:
type GetKeys<T> = T extends unknown[] ? T extends [] // special case empty tuple => no keys ? never : "0" extends keyof T // any tuple with at least one element ? Exclude<keyof T, keyof []> : number // other array : keyof T; // not an array type TupleKeys = GetKeys<[1, 2, 3]>; // "0" | "1" | "2" type EmptyTupleKeys = GetKeys<[]>; // never type ArrayKeys = GetKeys<string[]>; // number type RecordKeys = GetKeys<{ x: 1; y: 2; z: 3 }>; // "x" | "y" | "z" type GetValues<T> = T[GetKeys<T> & keyof T]; type TupleValues = GetValues<[1, 2, 3]>; // 1 | 2 | 3 type EmptyTupleValue = GetValues<[]>; // never type ArrayValues = GetValues<string[]>; // string type RecordValues = GetValues<{ x: 1; y: 2; z: 3 }>; // 1 | 2 | 3
Not sure this is bulletproof, but at least solves my use case nicely. 😉
Reacted by Kyle Anthony WilliamsReacted by Kyle Anthony WilliamsI'm running into a similar issue:
type MapTuple <T extends any[]> = { [K in keyof T]: { value: T[K] } } type Foo<T extends (...args: any[]) => any> = MapTuple<Parameters<T>> const f = <T extends (...args: any[]) => any>(a: Foo<T>): Parameters<T> => { return a.map(v => v.value) } // 'map' inferred as { value: Parameters<T>[never]; } and not callable.
Is there a work around in this case? 👀
Andarist commented
on Mar 13, 2023 ContributorMore actions@Deadalusmask I feel like your issue is likely related to another issue
Reacted by RubickIs the following issue related to this one as well? It shows that inlining a type mapper can lead to iteration over all array properties and methods.
type InnerMapper<Attributes> = { [Index in keyof Attributes]: Attributes[Index]; }; //--- The inlined mapper MapperA2 yields different results when the attributes are handed in via an interface interface AttributeWrapper { attributes: string[]; } type MapperA1<Wrapper extends AttributeWrapper> = InnerMapper<Wrapper['attributes']>; type MapperA2<Wrapper extends AttributeWrapper> = { [Index in keyof Wrapper['attributes']]: Wrapper['attributes'][Index]; }; type ResultA1 = MapperA1<{ attributes: ['type', 'id'] }>; // ^? type ResultA1 = ["type", "id"] type ResultA2 = MapperA2<{ attributes: ['type', 'id'] }>; // ^? type ResultA2 = { [x: number]: "type" | "id"; 0: "type"; 1: "id"; length: 2; toString: () => string; ... //--- Results of both mappers are identical when attributes are handed in directly type MapperB1<Attr extends string[]> = InnerMapper<Attr>; type MapperB2<Attr extends string[]> = { [Index in keyof Attr]: Attr[Index]; }; type ResultB1 = MapperB1<['type', 'id']>; // ^? type ResultB1 = ["type", "id"] type ResultB2 = MapperB2<['type', 'id']>; // ^? type ResultB1 = ["type", "id"]
Compiler Options
{ "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictPropertyInitialization": true, "strictBindCallApply": true, "noImplicitThis": true, "noImplicitReturns": true, "alwaysStrict": true, "esModuleInterop": true, "declaration": true, "target": "ES2017", "jsx": "react", "module": "ESNext", "moduleResolution": "node" } }Playground Link: Provided
New Workaround for Tuples
Ran into this too, but I found a more concise and simpler work around using recursive types with the bonus of tail-recursion optimization:
Fix:
type TupleKeysToUnion<T extends readonly unknown[], Acc = never> = T extends readonly [infer U, ...infer TRest] ? TupleKeysToUnion<TRest, Acc | U> : Acc; export type MapOfTupleKeys<T extends readonly unknown[]> = { [K in Extract<TupleKeysToUnion<T>, PropertyKey>]: K };
Example that turns a tuple into a map of keys with itself:
type Example = ['a', 'b', 'c']; type KeysAsUnion = TupleKeysToUnion<Example>; //result: "a" | "b" | "c" type MappedResult = MapOfTupleKeys<Example>; // type MappedResult = { // a: "a"; // b: "b"; // c: "c"; // }
Reacted by Paul Grau and Sam VervaeckReacted by Joe CalzarettaNew Workaround for Tuples
Ran into this too, but I found a more concise and simpler work around using recursive types with the bonus of tail-recursion optimization:
Fix:
type TupleKeysToUnion<T extends readonly unknown[], Acc = never> = T extends readonly [infer U, ...infer TRest] ? TupleKeysToUnion<TRest, Acc | U> : Acc; export type MapOfTupleKeys<T extends readonly unknown[]> = { [K in Extract<TupleKeysToUnion<T>, PropertyKey>]: K };
Example that turns a tuple into a map of keys with itself:
type Example = ['a', 'b', 'c']; type KeysAsUnion = TupleKeysToUnion<Example>; //result: "a" | "b" | "c" type MappedResult = MapOfTupleKeys<Example>; // type MappedResult = { // a: "a"; // b: "b"; // c: "c"; // }
Very much letters
type Example = ['a', 'b', 'c']; type KeysAsUnion = Example[number]; type MapOfTupleKeys<T extends [...any]> = { [K in T[number]]: K } type MappedResult = MapOfTupleKeys<Example>;I've come up with a more concise and straightforward method that should be able to address all possible scenarios.
type GetKeys<T> = ( T extends readonly unknown[] ? T extends Readonly<never[]> ? never : { [K in keyof T]-?: K }[number] : T extends Record<PropertyKey, unknown> ? { [K in keyof T]-?: K extends number | string ? `${K}` : never }[keyof T] : never ) type TupleKeys = GetKeys<['hello', 'world']> // "0" | "1" type ArrayKeys = GetKeys<string[]> // number type EmptyTupleKeys = GetKeys<[]> // never type RecordKeys = GetKeys<{ a: 1, b: 2, 100: 'aaa', 200: 'bbb' }> // "a" | "b" | "100" | "200" type UnionKeys = GetKeys<{ id: number, a: 1 } | { id: number, b: 2 }> // "a" | "b" | "id"Reacted by Jrson and SuperOleg39
TypeScript Version: 3.2.0-dev.20181019
Search Terms: mapped tuples reify
Code
Expected behavior: Baz should be [string, number]
Actual behavior: Type '["a", "b"][K]' cannot be used to index type 'Bar'.
Playground Link: https://www.typescriptlang.org/play/index.html#src=type%20Foo%20%3D%20%5B'a'%2C%20'b'%5D%3B%0D%0Ainterface%20Bar%0D%0A%7B%0D%0A%09a%3A%20string%3B%0D%0A%09b%3A%20number%3B%0D%0A%7D%0D%0A%0D%0Atype%20Baz%20%3D%20%7B%20%5BK%20in%20keyof%20Foo%5D%3A%20Bar%5BFoo%5BK%5D%5D%3B%20%7D%3B%20%2F%2F%20Expected%20Baz%20to%20be%20%5Bstring%2C%20number%5D%0D%0A%0D%0Atype%20WorkingBaz%20%3D%20%7B%20%5BK%20in%20Exclude%3Ckeyof%20Foo%2C%20keyof%20any%5B%5D%3E%5D%3A%20Foo%5BK%5D%20extends%20keyof%20Bar%20%3F%20Bar%5BFoo%5BK%5D%5D%20%3A%20never%3B%20%7D%20%26%20%7B%20length%3A%20Foo%5B'length'%5D%3B%20%7D%20%26%20any%5B%5D%3B
Related Issues: #25947
Given the Mapped tuple types feature (#25947). I'd expect the code above to work cleanly.
However, I still need to do:
To have an equivalent type. As far as I understand, the "K" in a mapped type on a tuple should iterate only on numeric keys of this tuple. Therefore, Foo[K] should always be a valid key for Bar...