Inferovať typ vlastnosti rozhrania v TypeScripte


Povedzme, že mám toto rozhranie a objekt, ktorý obsahuje tento typ:

interface IMyData {
    a: TypeA;
    b: TypeB;
    c: boolean;
    d: string;
}

const myObj: { data: IMyData } = {
    data: {
         a: someValueA,
         b: someValueB,
         c: true,
         d: "someValueD"
    }
}

Teraz by som chcel získať jednu vlastnosť z toho objektu a nechať funkciu odvodiť návratový typ:

function getField(obj: { data: IMyData }, field: keyof IMyData){
    return obj.data[field];
}

const myFieldStr = getField(myObj, "d"); // should infer type string
const myFieldBool = getField(myObj, "c"); // should infer type boolean

Ako môžem definovať funkciu getField tak, aby odvodzovala typy návratu? Momentálne by odvodzovala TypeA | TypeB | boolean | string.

Tu je ďalší (zložitejší?) scenár:

interface IMyValue<T> {
    value?: T;
}

interface IMyData2 {
    a?: IMyValue<string>;
    b?: IMyValue<number>;
    c?: IMyValue<boolean>;
    d?: IMyValue<string>;
}

function getValue<T extends keyof IMyData2>(field: T, data: IMyData2) {
    return data[field] ? data[field]!.value : undefined; // this wouldn't compile without '!' mark
}

const testData: IMyData2 = {
    a: { value: 'a' },
    b: { value: 2 },
    c: { value: false },
};

const testValue1 = getValue('a', testData); // i want this to detect as type of `string`
const testValue2 = getValue('b', testData); // i want this to detect as type of `number`
const testValue3 = getValue('b', testData); // i want this to detect as type of `boolean`
const testValue4 = getValue('b', testData); // i want this to detect as type of `undefined`

Odpoveď č. 1

Musíte typovému systému oznámiť presnú doslovnú hodnotu field. Najjednoduchší spôsob, ako to urobiť, je použiť generickú funkciu, ako je táto:

interface IMyData {
    c: boolean;
    d: string;
}

const myObj: { data: IMyData } = {
    data: {
         c: true,
         d: "someValueD"
    }
}

function getField<T extends keyof IMyData>(obj: { data: IMyData }, field: T){
    return obj.data[field];
}

const myFieldStr = getField(myObj, "c"); // should infer type boolean
const myFieldBool = getField(myObj, "d"); // should infer type string

Alebo v najjednoduchšom všeobecnom prípade:

function pluck<T, K extends keyof T>(obj : T, key : K) {
    return obj[key];
}

const foo = pluck({ bar: "asd", baz: 5 }, "bar"); // infers string

Odpoveď č. 2

Možným riešením druhého (zložitejšieho) scenára by bolo definovať nový typ, ktorý extrahuje typ z generika:

type GenericOf<S> = S extends IMyValue<infer T> ? T : never;

Teraz vy "zapojíte" tento typ vašej funkcie:

function getValue<T extends keyof IMyData2>(field: T, data: IMyData2) {
    // old solution: return data[field] ? data[field]!.value : undefined;
    return data[field]?.value as GenericOf<IMyData2[T]>;
}

To bude mať za následok, že návratové hodnoty funkcie budú správne typované:

const testValue1 = getValue('a', testData); // string | undefined
const testValue2 = getValue('b', testData); // number | undefined
const testValue3 = getValue('c', testData); // boolean | undefined
const testValue4 = getValue('d', testData); // string | undefined

Toto nie je najvkusnejšie riešenie, ale funguje to!