从键值对中推断类型,其中值是函数签名? [英] Infer types from key value pairs, where the values are function signatures?

查看:62
本文介绍了从键值对中推断类型,其中值是函数签名?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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!

但是由于这些语句,我们没有任何类型强制.

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

A LOWA

使用关键字

解决方案

您应该考虑的第一件事是不要将configObject类型设置为ConfigObject,因为这样会丢失对象的结构.创建扩展ConfigObject的具体接口:

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

MyThing中要摆脱any,您可以结合几个TS功能从configObject中提取类型:

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

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

打字稿游乐场

P.S.从T中提取类型看起来很丑,并且可以做一些优化,我只是为说明目的而明确编写了它.

I'm creating a mapping like function that is going to turn an object like this:

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


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

into:

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


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

The issue I'm having is around enforcing the function parameter and return types, based on the ConfigItem's signatures.

The way I'm doing it looks like this:

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

I later implement a function to create a MyThing like:

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

    };
}

Now this code works:



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!

But we don't have any type enforcement, due to those any statements.

How would I modify my code to make this work?

Full TypeScript playground here.

Second attempt at a solution using the infer keyword

解决方案

First thing you should consider is to not set configObject type to ConfigObject, because you lose the structure of the object. Create concrete interface which extends ConfigObject instead:

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

In MyThing to get rid of anys you can extract types from configObject with combining several TS features:

  • Parameters<T> - Constructs a tuple type of the types of the parameters of a function type T
  • ReturnType<T> - Constructs a type consisting of the return type of function T
  • Index types - With index types, you can get the compiler to check code that uses dynamic property names. For example, to pick a subset of properties
  • Mapped Types - Mapped types allow you to create new types from existing ones by mapping over property types

With above we extract argument and return types from oneWay and otherWay methods to set into 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]>;
}

TypeScript Playground

P.S. Extracting types from T looks ugly and there can be done some optimizations, I've just written it explicitly for illustration purpose.

这篇关于从键值对中推断类型,其中值是函数签名?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆