Home > Back-end >  Narrowing of generic types for mapping functions
Narrowing of generic types for mapping functions

Time:04-30

I'd expect recent versions of TS (see for example #43183) to make this pattern possible without unsafe casting. Any ideas? (playground)

export type Mapping = {
    number: number;
    string: string;
};

function get<K extends keyof Mapping>(key: K): Mapping[K] {
    switch (key) {
        case "number":
            // ERROR: Type 'number' is not assignable to type 'Mapping[K]'
            return 42; // Adding "as Mapping[K]" works, but that's not type-safe
        case "string":
            // ERROR: Type 'string' is not assignable to type 'Mapping[K]'
            return "hello";
        default:
            throw "Never reached, but otherwise TS throws: Function lacks...";
    }
}

const usageWorksAsExpected = { n: get("number"), s: get("string") };

CodePudding user response:

Your code corresponds to Suggestion for Dependent-Type-Like Functions: Conservative Narrowing of Generic Indexed Access Result Type #33014

The issue is not yet resolved, but one of the comments suggests following workaround:

export type Mapping = {
    number: number;
    string: string;
};

function get<K extends keyof Mapping>(key: K): Mapping[K]
function get(key: keyof Mapping) : Mapping[keyof Mapping] {
    switch (key) {
        case "number": {
            const ret: Mapping[typeof key] = 42;
            const ret1: Mapping[typeof key] = 'hello'; // Expected error
            return ret;
        }
        case "string": {
            const ret: Mapping[typeof key] = 'hello';
            return ret;
        }
        default:
            throw "Never reached, but otherwise TS throws: Function lacks...";
    }
}

const usageWorksAsExpected = { n: get("number"), s: get("string") };

Playground link

CodePudding user response:

No, the support for contextual narrowing of generics added in TypeScript 4.3, as implemented in microsoft/TypeScript#43183, doesn't do what you are expecting. This support narrows the type of the key from K to either "number" or "string", but it does not narrow the type of the type parameter K itself. Even if key is confirmed to be "number", the type parameter K is unchanged, and so the compiler cannot verify that 42 is assignable to Mapping[K]. It's frustrating.

There are some open issues, like microsoft/TypeScript#33014 and microsoft/TypeScript#27808 which propose ways by which the compiler might narrow K itself. But these have not been implemented yet.

If you'd like to rewrite get() so that it compiles with no errors and without giving up type safety, then you can do it like this:

function get<K extends keyof Mapping>(key: K): Mapping[K] {
    return {
        number: 42,
        string: "hello"
    }[key] // okay
}

Your return type is a generic indexed access type Mapping[K]. The compiler can verify that if you index into a value of type Mapping with a key of type K, you'll get a value of type Mapping[K], so that's why the above rewritten code works.

This relies on getting ahold of a value of type Mapping. Sometimes you can't or don't want to do this because it forces you to eagerly pre-calculate the results for every possible input when you don't want to. If so, you could implement getters instead so that the Mapping object is lazily computed:

function get<K extends keyof Mapping>(key: K): Mapping[K] {
    return {
        get number() { return 42 },
        get string() { return "hello" }
    }[key]
}

Playground link to code

  • Related