Home > Mobile >  Typescript: Enforcing property type based on another property value which is a key of a generic
Typescript: Enforcing property type based on another property value which is a key of a generic

Time:05-15

Given the following type:

export type MyType = {
    a: boolean;
    b: number;
}

I want to create a type:

export type Mapper<T> = {
    key: keyof T;
    func: (value: string) => T[key];
}

So that I can have:

const mapToMyTypePropertyA: Mapper<MyType> = {
    key: "a",
    func: v => !!v // enforce output type to be the type of MyType["a"], which is boolean
};

const mapToMyTypePropertyA: Mapper<MyType> = {
    key: "b",
    func: v => v.length // enforce output type to be the type of MyType["b"], which is number
};

i.e. I want to enforce the return type of func (which is a function) to be the type of a property of a generic type determined by the key (which is a keyof the generic type).

Is this possible?

I could only get to this so far:

export type Mapper<T, K extends keyof T> = {
    key: K;
    func: (value: string) => T[K];
}
const mapToMyTypePropertyA: Mapper<MyType, "a"> = {
    key: "a",
    func: v => !!v // I get the enforcement here
}

But this requires repeating the key twice in the generic type and in the property key.

CodePudding user response:

You can use another type to "generate" the possible objects:

type GenerateCombos<T> = {
    [K in keyof T]: {
        key: K;
        func: (value: string) => T[K];
    };
}[keyof T];

This will give us a union of the allowed objects. For example, GenerateCombos<{ foo: string }> would only give us:

{ key: "foo"; func: (value: string) => string }

And GenerateCombos<MyType> gives us the nice type:

{
    key: "a";
    func: (value: string) => boolean;
} | {
    key: "b";
    func: (value: string) => number;
}

To use it, you can give it an alias first:

type Mapper = GenerateCombos<MyType>;

Then annotate the types of variables:

const mapToMyTypeProperty1: Mapper = {
    key: "a",
    func: v => !!v // enforce output type to be the type of MyType["a"], which is boolean
};

const mapToMyTypeProperty2: Mapper = {
    key: "b",
    func: v => v.length // enforce output type to be the type of MyType["b"], which is number
};

And it also errors if you do it incorrectly:

const mapToMyTypeProperty3: Mapper = {
    key: "a",
    func: v => v // ! error
};

const mapToMyTypeProperty4: Mapper = {
    key: "b",
    func: v => !!v // ! error
};

Try this out here.

CodePudding user response:

Another solution is to use a utility function to make TypeScript try to infer the generics for us:

function mapper<K extends keyof MyType>(mapper: Mapper<MyType, K>) { return mapper; }

It's very easy to use, just call the function with the object as you would normally:

const mapToMyTypeProperty1 = mapper({
    key: "a",
    func: v => !!v // enforce output type to be the type of MyType["a"], which is boolean
});

const mapToMyTypeProperty2 = mapper({
    key: "b",
    func: v => v.length // enforce output type to be the type of MyType["b"], which is number
});

And it errors if you do it incorrectly, too:

const mapToMyTypeProperty3 = mapper({
    key: "a",
    func: v => v // ! WRONG TYPE
});

const mapToMyTypeProperty4 = mapper({
    key: "b",
    func: v => !!v // ! WRONG TYPE
});

You can play around with this more here

  • Related