typescript - 如何计算打字稿中对象的动态键类型
问题描述
我开始使用 Typescript,但不知道如何根据函数输入动态地在对象上设置粒度类型。
type Options<T1, T2> = {
name: string;
transforms: Record<string, {
from: (val: T1) => T2,
to: (val: T2) => T1,
}>;
};
type IO<T> = {
read:() => T;
write: (value: T) => void;
}
function transformer<T1, T2>(options: Options<T1, T2>, value: T1) {
const { name, transforms = {}} = options;
// How do I type this as follows:
// - Has key `name` and return type `IO<T1>`
// - Transforms have key [dynamic name], and whatever IO<"return value of from">
type Values = {
[name/* exact value of name?? */]: IO<T1>,
[FromReturnType<tr>/*??*/ in transforms]: IO<tr>,
};
const values: Values = {
[name]: {
read: () => value,
write: (v) => value = v,
},
};
for (const transform in transforms) {
const { to, from } = transforms[transform];
values[transform] = {
read: () => from(value),
write: (v) => value = to(v),
};
}
return values;
}
然后,消费者将按如下方式使用该函数:
// Usage
const { number, string, ...otherTypedTransforms } = transformer({
name: "number",
transforms: {
string: {
to: Number,
from: String,
},
// Other transforms with different `from` types
// ...
},
}, 0);
希望 tsc 可以根据转换推断number.read()
为类型number
和string.read()
类型string
等from()
。
我已经完成了许多关于 SO 的问答,但都没有成功。
解决方案
这是我给transformer()
呼叫签名的打字:
declare function transformer<K extends string, I, O extends object>(options: {
name: K,
transforms: { [P in keyof O]: { from: (val: I) => O[P], to: (val: O[P]) => I } }
}, value: I): { [P in keyof AddProp<O, K, I>]: IO<AddProp<O, K, I>[P]> };
type AddProp<T extends object, K extends PropertyKey, V> = Record<K, V> & T;
在我解释它之前,让我们通过一个示例调用确保它符合您的要求:
const transformed = transformer({
name: "number",
transforms: {
string: {
to: Number,
from: String,
},
date: {
to: d => d.getTime(),
from: (n: number) => new Date(n)
}
},
}, 0);
/* const transformed: {
number: IO<number>;
string: IO<string>;
date: IO<Date>;
} */
看起来不错,对吧?
因此,该transformer()
函数在三个类型参数中是通用的:
K extends string
是对象的字段name
类型options
。此类型参数需要在此处name
,以便您可以跟踪. 否则,如果您将其保留string
在您的Options
类型中,编译器将忘记它。I
是value
参数的类型,是每个变换from()
方法的输入(以及它的to()
方法的输出)。O extends object
是一个对象,其属性是对象的键transforms
,其值是相应变换from()
方法的输出(及其to()
方法的输入)。不会有实际的类型值O
传入transformer()
,但编译器将能够从 中推断出它transforms
。用参数表示transforms
参数和函数返回类型O
比transforms
直接用参数表示返回类型要容易得多(这将涉及各种类型查询)。
对象的transforms
属性options
是一个映射类型,我们获取每个属性键P
并O
使用其值类型O[P]
来构造from
/to
方法类型。
对于函数的返回类型,我们首先增加O
一个键为 typeK
且值为 type的属性I
。(我AddProp
为此使用了 type 函数。)然后我们通过获取每个属性并用IO<>
.
这就是打字。至于实现,您几乎不可能让编译器验证它是否符合如此复杂的调用签名。因此,最好的方法是非常小心地正确实现它,并使用类型断言的等价物来抑制编译器错误。在这种情况下,我倾向于使用具有所需类型的单个调用签名的重载函数any
,并且其实现签名明显宽松(在必要时大量使用):
// call signature
function transformer<K extends string, I, O extends object>(options: {
name: K,
transforms: { [P in keyof O]: { from: (val: I) => O[P], to: (val: O[P]) => I } }
}, value: I): { [P in keyof AddProp<O, K, I>]: IO<AddProp<O, K, I>[P]> };
// implementation
function transformer(options: {
name: string,
transforms: Record<string, { from: (val: any) => any, to: (val: any) => any }>
}, value: any) {
const { name, transforms = {} } = options;
const values = {
[name]: {
read: () => value,
write: (v: any) => value = v,
},
};
for (const transform in transforms) {
const { to, from } = transforms[transform];
values[transform] = {
read: () => from(value),
write: (v) => value = to(v),
};
}
return values;
}
让我们看看编译器和运行时是否一致,使用上面的测试:
console.log(transformed.date.read().getFullYear()); // 1969
transformed.date.write(new Date());
console.log(transformed.number.read()); // 1627655089690
console.log(transformed.string.read()); // "1627655089690"
看起来挺好的!
推荐阅读
- ios - Swift 4.2 自定义字体自动调整大小
- ruby-on-rails - 通过双重嵌套字段引用返回错误结果
- gcc - 为什么 snprintf/vsnprintf 在每次使用 %n 调用时都打开 /proc/self/maps?
- java - 如何在 Android Studio 上使用 Mikrotik API
- matlab - 如何在 MATLAB 中绘制二维元素网格的轮廓?
- amazon-web-services - 如何使用 2 个路径参数创建端点。AWS API 网关
- c# - EF Core 从具有多重性零或一的模型中获取实体的导航属性
- oracle - 如何在 PL/SQL (Oracle) 中发送电子邮件?
- angular - 基于 Angular 地图的应用程序的后端技术和架构
- symfony - 找不到条目“虚拟”...EntrypointLookup.php