首页 > 解决方案 > 从键值对推断类型,其中值是函数签名?

问题描述

我正在创建一个类似函数的映射,它将像这样转换一个对象:

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


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

进入:

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


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

我遇到的问题是根据 ConfigItem 的签名强制执行函数参数和返回类型。

我这样做的方式是这样的:

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>>
}

我后来实现了一个创建 MyThing 的函数,例如:

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>>

    };
}

现在这段代码有效:



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!

但是由于这些 any 声明,我们没有任何类型强制执行。

我将如何修改我的代码以使其工作?

完整的 TypeScript 游乐场在这里。

infer使用关键字第二次尝试解决方案

标签: typescriptgenerics

解决方案


您应该考虑的第一件事是不要将configObjecttype 设置为ConfigObject,因为您会丢失对象的结构。创建扩展的具体接口ConfigObject

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

MyThing摆脱s 时,您可以通过组合几个 TS 功能any来提取类型:configObject

  • Parameters<T>- 构造函数类型 T 的参数类型的元组类型
  • ReturnType<T>- 构造一个由函数 T 的返回类型组成的类型
  • Index types- 使用索引类型,您可以让编译器检查使用动态属性名称的代码。例如,选择属性的子集
  • Mapped Types- 映射类型允许您通过映射属性类型从现有类型创建新类型

oneWay上面我们从和方法中提取参数和返回类型otherWay以设置为Foo<A, B>and 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]>;
}

打字稿游乐场

PS 从 T 中提取类型看起来很难看,并且可以进行一些优化,我只是为了说明目的而明确编写了它。


推荐阅读