Sascha Hoppe
Sascha Hoppe

Reputation: 43

How to get type of specific "keyof" parameter in Typescript?

I'd like to define an Adapter Schema, to convert one Object into another.

export type AdapterSchema<Source, Destination> = {
    [key in keyof Partial<Source>]: {
        target: keyof Destination;
        convert: (source: Source[key]) => Destination[keyof Destination];
    };
};

It should be possible to add a convert function, that converts the value of a property into the value of the target property.

interface Foo {
   a : string;
}

interface Bar {
   b : number;
   c : string;
}

const schema : AdapterSchema<Foo,Bar> = { a : {target: 'b', convert: (value) => 'x'}} 

I want the compiler to throw an error here, because 'x' is not of type number. My type is at least a bit type safe, because only types of Bar are allowed, but no error is thrown, because Bar has also a property of type string.

Can this be made more type safe?

Thanks in advance.

Upvotes: 3

Views: 752

Answers (1)

jcalz
jcalz

Reputation: 327934

You will want to change AdapterSchema<S, D> so that each property is a union of the different possible target/convert pairs for each key KD in the D type.

That is, whatever AdapterSchema<S, D> is, you want AdapterSchema<Foo, Bar> to evaluate to:

type ASFB = AdapterSchema<Foo, Bar>;
/* type ASFB = {
    a?: {
        target: "b";
        convert: (source: string) => number;
    } | {
        target: "c";
        convert: (source: string) => string;
    } | undefined;
} */

We can do this by changing your version (slightly rewritten but it's the same):

type OrigAdapterSchema<S, D> = {
    [KS in keyof S]?: {
        target: keyof D;
        convert: (source: S[KS]) => D[keyof D];
    };
};

to the following version:

type AdapterSchema<S, D> = {
    [KS in keyof S]?: ({ [KD in keyof D]-?: {
        target: KD;
        convert: (source: S[KS]) => D[KD];
    } }[keyof D]);
}

This works by building a mapped type where each key KD of D is given a property where target is KD and where convert's return type is D[KD]... and then immediately indexing into it with keyof D to get the union of properties.

You can verify that now AdapterSchema<Foo, Bar> is the desired type. And now the compiler behaves as you wanted:

const badSchema: AdapterSchema<Foo, Bar> = {
    a: { target: 'b', convert: (value) => 'x' } // error!
    //   ~~~~~~ <-- b is not assignable to c
}

const schema: AdapterSchema<Foo, Bar> = {
    a: {
        target: "b",
        convert: v => v.length
    }
}; // okay

(Note that this solution does not forceAdapterSchema<S, D> to really produce every property in D; for example {} is an acceptable AdapterSchema<Foo, Bar>, or you could have each property in S map to the same property in D. But your question is not about this, so any answers to this are out of scope here.)

Playground link to code

Upvotes: 1

Related Questions