2

Now that typescript 3.1 introduced mapped tuple types, I was hoping this code sample would work:

export interface SettingKey {
    General_Language: 'en' | 'sl';
    Map_InitialLongitude: number;
    Map_InitialLatitude: number;
}

export function fetchSetting<K extends (keyof SettingKey)[]>
        (...keys: K): Promise<SettingKey[K]> {
    return null as any;
}
fetchSetting('General_Language', 'Map_InitialLongitude').then(x => {
    return x['General_Language'] === 'de' // would want compilation error 'de' not in 'en' | 'sl'
})

But it doesn't. The errors are:

ttt.ts:7:83 - error TS2536: Type 'K' cannot be used to index type 'SettingKey'.

7 export function fetchSetting<K extends (keyof SettingKey)[]>(...keys: K): Promise<SettingKey[K]> {
                                                                                    ~~~~~~~~~~~~~

ttt.ts:11:12 - error TS2571: Object is of type 'unknown'.

11     return x['General_Language'] === 'de'
          ~

Clearly the second error is a consequence of the first one, so that's not really a concern. The first one is the problematic one.

keys is an array of keyof SettingKey, and so I would hope that SettingKey[K] would be an array of the types of the listed properties (so, concretely in the code sample I put, it would be ['en' | 'sl', number]. From the pull request introducing the typescript feature:

If T is an array type S[] we map to an array type R[], where R is an instantiation of X with S substituted for T[P].

But that holds I guess for mapped types only, and here I have a lookup type, that would be the reason why it doesn't work I guess?

I think what I want to express is clear; can this be made type-safe in typescript?

Emmanuel Touzery
  • 9,008
  • 3
  • 65
  • 81
  • 2
    If you want `['en' | 'sl', number]` then you should be looking up `x[0]`, not `x['General_Language']`. That's a separate error from the one about mapping tuples. – jcalz Nov 10 '18 at 19:39

1 Answers1

4

To have a mapped tuple you need a mapped type, that will map the original tuple (in the type parameter K) to the new tuple type

export interface SettingKey {
    General_Language: 'en' | 'sl';
    Map_InitialLongitude: number;
    Map_InitialLatitude: number;
}

type SettingKeyProp<P extends keyof SettingKey> = SettingKey[P]
type SettingKeyArray<K extends { [n: number]: keyof SettingKey }> = {
  [P in keyof K]: K[P] extends keyof SettingKey ? SettingKey[K[P]]: never 
} 
export function fetchSetting<K extends (keyof SettingKey)[]>
        (...keys: K): Promise<SettingKeyArray<K>> {
    return null as any;
}
fetchSetting('General_Language', 'Map_InitialLongitude').then(x => {
    // x[0] is 'en' | 'sl'
    return x[0] === 'de' /// since you want a tuple, you should index by number not name
})

If you want to index by name that is also possible, but the mapped type should map over the values in the array not the keys in the array:

type SettingKeyArray<K extends { [n: number]: keyof SettingKey }> = {
  [P in K[number]]: SettingKey[P] 
} 
export function fetchSetting<K extends (keyof SettingKey)[]>
        (...keys: K): Promise<SettingKeyArray<K>> {
    return null as any;
}
fetchSetting('General_Language', 'Map_InitialLongitude').then(x => {
    // you can access by name
    return x.General_Language === 'de' 
}) 
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 3
    great and thank you! Note that the type parameter for `SettingKeyArray` looks a little frightening, I found out that saying `type SettingKeyArray = ...` works just as well. – Emmanuel Touzery Nov 11 '18 at 08:30