从键值对中推断类型,其中值是函数签名? [英] Infer types from key value pairs, where the values are function signatures?
问题描述
我正在创建一个类似函数的映射,它将像这样旋转一个对象:
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!
但是由于这些语句,我们没有任何类型强制.
我将如何修改我的代码以使其正常工作?
您应该考虑的第一件事是不要将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
-映射类型允许您通过映射属性类型从现有类型创建新类型
上面,我们从oneWay
和otherWay
方法中提取参数并返回类型以设置为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 any
s 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 TReturnType<T>
- Constructs a type consisting of the return type of function TIndex types
- With index types, you can get the compiler to check code that uses dynamic property names. For example, to pick a subset of propertiesMapped 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]>;
}
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屋!