简体   繁体   中英

How to address a mapped type with a generic index in Typescript?

I'm trying to access a property from a mapped type using a generic type. But I'm getting the error in the comment bellow. What it the correct way to type something like this?

type ThingType = 'typeA' | 'typeB';

interface Thing<T extends ThingType> {
    type: T,
    name: string
};

type Container<T extends ThingType> = {
    [id: string]: Thing<T>
}

type Store<T extends ThingType = ThingType> = {
    [K in T]?: Container<K>
};

const myStore: Store = {
    'typeA': {
        'one': {type: 'typeA', name: 'one'},
        'two': {type: 'typeA', name: 'two'}
    }
};

// This one does not fail
const typeAContainer: Container<'typeA'> | undefined = myStore['typeA'];

function storeThing<T extends ThingType>(thing: Thing<T>) {
    // Error here:
    const container: Container<T> | undefined = myStore[thing.type];
    // Type 'Store<ThingType>[T]' is not assignable to type 'Container<T> | undefined'.
    //  Type 'Container<"typeA"> | Container<"typeB"> | undefined' is not assignable to type 'Container<T> | undefined'.
    //    Type 'Container<"typeA">' is not assignable to type 'Container<T>'.
    //      Type '"typeA"' is not assignable to type 'T'.
    //        '"typeA"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'ThingType'.
    //          Type 'Store<ThingType>[T]' is not assignable to type 'Container<T>'.
    //            Type 'Container<"typeA"> | Container<"typeB"> | undefined' is not assignable to type 'Container<T>'.
    //              Type 'undefined' is not assignable to type 'Container<T>'.

    // ... code here ...
}

storeThing({type: 'typeA', name: 'three'});

The compiler doesn't really perform a whole lot of higher-level analysis required to confirm the assignability of types that depend on as-yet-unspecified generic type parameters. Inside the implementation of storeThing , you are assigning a value of type Store<ThingType>[T] to a variable of type Container<T> | undefined Container<T> | undefined , where T has not yet been specified as a concrete type. And unfortunately the compiler cannot see those as compatible without specifying T , even though they are compatible for all possible narrowings of T .

This is essentially a design limitation of TypeScript. See microsoft/TypeScript#36737 and microsoft/TypeScript#36349 for similar issues of this sort; in both cases, the compiler cannot follow the higher-order relationship between a generic indexed access and another compatible type. There is an existing open suggestion, microsoft/TypeScript#33014 , to handle cases like this better, but it's not clear if anything is going to be implemented there.

Until and unless that happens, we have to come up with some way forward.


A reasonable way to proceed here: once you have well and truly convinced yourself that what you are doing is perfectly safe (and be careful; it's easy to get this wrong and think that something is safe when it's not), a judicious use of a type assertion is appropriate:

const container = myStore[thing.type] as Container<T> | undefined; // okay

This sort of thing is the intended use case for type assertions: situations in which you know more about the type than the compiler can verify. You just assert that what you're doing is safe, and move on. There's some danger, of course, that you have lied to the compiler... or that your code will change in the future and turn the previously correct type assertion into a lie. Type assertions shift the burden of verifying type safety from the compiler to the developer, which is fine as long as you're willing to accept that responsibility.


Another way to proceed: find some type manipulations which are self-contained enough for the compiler to follow your reasoning even with an unspecified generic. This is a bit of an art, and it's not always possible. Here what I would do is this two-step process:

const myStoreNarrowed: Store<T> = myStore; // okay
const container: Container<T> | undefined = myStoreNarrowed[thing.type]; // okay

The compiler is able to recognize that it's safe to assign myStore to a variable of type Store<T> , which is narrower than Store<ThingType> . And then, it's able to recognize that when you index into Store<T> with a T key you get something assignable to Container<T> | undefined Container<T> | undefined .

I'd probably use this approach since it still gives you some type safety guarantees that are lost when you use a type assertion. But if all else fails, there's always a (carefully thought out) type assertion.


Okay, hope that helps; good luck!

Playground link to code


The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM