typescripttype-conversionindex-signature

How to extract defined keys in a type with index signature


TL;DR

Question: How can I create a type converter that gets the defined keys of objects typed with types with index signatures?


I want to create a type "converter" in TypeScript, which gets a type A and returns a new type B that have keys as all defined keys in A and accepts only string as values, like in the example:

type AToB<
   T,
   K extends keyof T = keyof T
> = { [id in K]: string };

const a = {
    x : {}
}

const b : AToB<typeof a> = {
    x : "here it works"
}

b works b works

But when I use this in a object that already has a type with index signature defined, the keyof doesn't get the defined keys, e.g.:

type AWithSig = {
    [id : string]: {};
}

const aSig : AWithSig = {
    y: {},
    z: {}
}

const bSig : AToB<typeof aSig> = {
    y : "here the keys",
    z : "are not shown :("
}

I tried it in the TSPlayground (link here) and it doesn't recognize the defined keys in aSig.

bSig doesn't work bSig doesn't work

bSig works if I define the keys manually bSig works if I define the keys manually


Solution

  • The problem isn't with AtoB, but with the type of aSig.

    When you annotate a variable with a (non-union) type, that's the type of the variable. It doesn't gain any more specific information from the expression with which you initialize the variable.

    So by the time you write const aSig: AWithSig = ⋯ you've already lost the type you care about.

    Presumably the only reason you annotated in the first place was just to check that the type of the initializing expression was a valid AWithSig. If so, you can instead use the satisfies operator on this expression to perform the check, and then just allow the compiler to infer the type of aSig afterward:

    const aSig = {
        y: {},
        z: undefined // error!
    //  ~ <-- undefined is not assignable to {}
    } satisfies AWithSig;
    

    Oops, I made a mistake and the compiler caught it:

    const aSig = {
        y: {},
        z: {}
    } satisfies AWithSig // okay
    

    And now the type of aSig is

    /* const aSig: {
        y: {};
        z: {};
    } */
    

    which will work as desired with AtoB:

    const bSig: AToB<typeof aSig> = {
        y: "here it doesn't work :(",
        z: "abc"
    }
    // const bSig: AToB<{ y: {}; z: {}; }, "y" | "z"> 
    

    Playground link to code