typescript - Inferă tipurile din perechi cheie-valoare, unde valorile sunt semnături de funcții?


Creez o funcție de tip mapare care va transforma un obiect ca acesta:

const configObject: ConfigObject = {
    a: {
        oneWay: (value: string) => 99,
        otherWay: (value: number) => "99"
    },


    b: {
        oneWay: (value: number) => undefined,
        otherWay: () => 99
    }
}

în :

{
    foos: {
        a: {
            convert: (value: string) => 99,
        },
        b: {
            convert: (value: number) => undefined
        }
    },


    bars: {
        a: {
            deconvert: (value: number) => "99",
        },
        b: {
            deconvert: () => 99;
        }
    }
}

Problema pe care o am este legată de aplicarea parametrilor funcției și a tipurilor de returnare, bazându-mă pe semnăturile lui ConfigItem.

Modul în care o fac arată astfel:

interface ConfigItem<P, Q> {
    oneWay: (value: P) => Q;
    otherWay: (value: Q) => P;
}

type ConfigObject = Record<string, ConfigItem<any, any>>; //This is right, I believe. 
// any is explicitly an OK type for the ConfigItems to have. 

interface Foo<A, B> {
    convert: (a: A) => B;
}

interface Bar<A, B> {
    deconvert: (b: B) => A;
}

interface MyThing<T extends ConfigObject> {
    foos: Record<keyof T, Foo<any, any>> //These are wrong - they should use the types as defined by the config object
    bars: Record<keyof T, Bar<any, any>>
}

Mai târziu implementez o funcție pentru a crea un MyThing astfel:

function createMyThing<T extends ConfigObject>(configObject: T): MyThing<T> {
    //I would use Object.entries, but TS Playground doesn't like it. 
    const keys = Object.keys(configObject);
    return {
        foos: keys.reduce((acc, key) => {
            return {
                ...acc,
                [key]: {
                    convert: configObject[key].oneWay
                }
            }
        }, {} as Record<keyof T, Foo<any, any>>), //Again problematic 'any' types. 

        bars: keys.reduce((acc, key) => {
            return {
                ...acc,
                [key]: {
                    deconvert: configObject[key].otherWay
                }
            };

        }, {}) as Record<keyof T, Bar<any, any>>

    };
}

Acum acest cod funcționează:


const configObject: ConfigObject = {
    a: {
        oneWay: (value: string) => 99,
        otherWay: (value: number) => "99"
    },


    b: {
        oneWay: (value: number) => undefined,
        otherWay: () => 99
    }
}
const myThing = createMyThing(configObject); 

console.log(myThing.foos.a.convert("hello"));  
console.log(myThing.foos.b.convert("hello"));  //No type enforcement!

Dar nu avem nicio aplicare a tipului, din cauza acelor declarații any.

Cum aș modifica codul meu pentru ca asta să funcționeze?

Întregul mediu de joacă TypeScript aici.

A doua încercare de a găsi o soluție folosind cuvântul cheie infer

Răspuns #1

Primul lucru pe care ar trebui să îl iei în considerare este să nu setezi tipul configObject la ConfigObject, pentru că îți pierzi structura obiectului. Creează în schimb o interfață concretă care extinde ConfigObject:

interface ConcreteConfigObject extends ConfigObject{
    a: ConfigItem<string, number>;
    b: ConfigItem<number, undefined>;
}

În MyThing pentru a scăpa de any puteți extrage tipuri din configObject combinând mai multe caracteristici TS:

  • Parameters<T> - Construiește un tip tuple din tipurile parametrilor unui tip de funcție T
  • ReturnType<T> - Construiește un tip constând din tipul de returnare al funcției T
  • Tipuri de index - Cu tipurile de index, poți determina compilatorul să verifice codul care folosește nume de proprietăți dinamice. De exemplu, pentru a alege un subset de proprietăți
  • Tipuri Mapate - Tipurile mapate vă permit să creați tipuri noi pornind de la cele existente prin maparea tipurilor de proprietăți

Cu cele de mai sus extragem tipurile de argument și de returnare din metodele oneWay și otherWay pentru a le seta în Foo<A, B> și Bar<A, B>:

interface MyThing<T extends ConfigObject> {
    foos: MyThingFoo<T>;
    bars: MyThingBar<T>;
}

type MyThingFoo<T extends ConfigObject> = {
    [k in keyof T]: Foo<Parameters<T[k]["oneWay"]>[0], ReturnType<T[k]["oneWay"]>>;
}

type MyThingBar<T extends ConfigObject> = {
    [k in keyof T]: Bar<ReturnType<T[k]["otherWay"]>, Parameters<T[k]["otherWay"]>[0]>;
}

Loc de joacă TypeScript

P.S. Extracția tipurilor din T arată urât și se pot face unele optimizări, am scris-o doar explicit în scop ilustrativ.

Răspunsul #2

În TypeScript este posibil să extragi semnătura unui tip dintr-o valoare existentă cu typeof:

const configObject = {
    a: {
        oneWay: (value: string) => 99,
        otherWay: (value: number) => "99"
    },


    b: {
        oneWay: (value: number) => undefined,
        otherWay: () => 99
    }
};

type ConfigObject = typeof configObject

Pe baza ConfigObject, poți crea MyThing astfel:

type MyThing = {
    foos: { [K in keyof ConfigObject]: { convert: ConfigObject[K]['oneWay']}}
    bars: { [K in keyof ConfigObject]: { deconvert: ConfigObject[K]['otherWay']}}
} 

Pentru completitudine, createMyThing poate fi tastat ca:

function createMyThing(configObject: ConfigObject) {
    const keys = Object.keys(configObject) as (keyof ConfigObject)[];

    return {
        foos: keys.reduce((acc, key) => {
            return {
                ...acc,
                [key]: {
                    convert: configObject[key].oneWay
                }
            }
        }, {}),

        bars: keys.reduce((acc, key) => {
            return {
                ...acc,
                [key]: {
                    deconvert: configObject[key].otherWay
                }
            };

        }, {}),
    } as MyThing;
}

Demonstrație