Search code examples
typescripttypesenumstypescript-genericsgeneric-function

TypeScript how associate enam with union types and get returned object property type?


I have an enum and its associated union type:

type User = { name: string, age: number }

export enum StorageTypeNames {
    User = "user",
    Users = "referenceInfo",
    IsVisibleSearchPanel = "searchPanel",
    PropertyPanelInfo = 'propertyPanelInfo'
};

type StorageType =
    | { name: StorageTypeNames.User, data: User }
    | { name: StorageTypeNames.Users, data?: User[] }
    | { name: StorageTypeNames.IsVisibleSearchPanel, data?: boolean }
    | { name: StorageTypeNames.PropertyPanelInfo, data: {visibility: boolean};

and there is a class using these types in the method of which I am trying to implement a generic function

export class StorageHelper {
    private static readonly _storage = window.localStorage;

    static get<T>(type: StorageType): T;
    static get<T>(type: StorageTypeNames): Pick<StorageType, 'data'>;
    static get<T>(type: StorageType | StorageTypeNames): T {
        let data: string | null = null;
        if(typeof type === 'string')
            data = this._storage.getItem(type);
        else
            data = this._storage.getItem(type.name);
        return data ? JSON.parse(data) : null;
    }
}

as a result I want to get this: depending on the incoming argument, the typescript suggested the properties of the outgoing object:

const userData = StorageHelper.get(StorageTypeNames.User).data. //typeScript dosent help

enter image description here

please tell me how to do it, thanks in advance


Solution

  • The call signatures of get() don't express what you're doing. It looks like you want to accept a parameter either of some subtype of StorageType, in which case you return a value of the same subtype; or of some subtype of StorageTypeNames, in which case you return a value of the corresponding member of StorageType.

    For the first case, you need get() to be generic in the type T of type, where T is constrained to StorageType. And then the return type is also T:

    static get<T extends StorageType>(type: T): T;
    

    Your mistake was by making type of type StorageType instead of type T constrained to StorageType, since then the compiler has no idea what T is.

    For the second case, you want get() to be generic in the type K of type, where K is now constrained to StorageTypeNames. But now you want to take the StorageType union and filter it to get just the one member whose name property is of type K. We can use the Extract<T, U> utility type to filter unions this way. Here's the call signature:

    static get<K extends StorageTypeNames>(type: K): Extract<StorageType, { name: K }>;
    

    And then you can implement the method however you want:

    static get(type: StorageType | StorageTypeNames) {
      /* impl */
    }
    

    (although if your method actually has a chance of returning null you should make the output type include the null type like T | null or Extract<StorageType, {name: K}> | null so that someone using the widely-recommended --strictNullChecks compiler option would benefit from the extra checking.)

    Let's test it out:

    StorageHelper.get(StorageTypeNames.User).data.age.toFixed(2); // okay
    StorageHelper.get(
      StorageHelper.get(StorageTypeNames.PropertyPanelInfo)
    ).data.visibility // okay
    

    Looks good. The compiler understands that, for example, get(StorageTypeNames.User) returns a value of type { name: StorageTypeNames.User, data: User }, which is what you wanted.

    Playground link to code