我刚开始使用 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 的问答,但都没有成功。
答案 0 :(得分:1)
这是我给 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
的 literal 字符串值。否则,如果您将其保留为 string
,就像在 Options
类型中一样,编译器会忘记它。
I
是 value
参数的类型,它是每个转换的 from()
方法的输入(及其 to()
方法的输出)。>
O extends object
是一个对象,其属性是 transforms
对象的键,其值是相应变换的 from()
方法的输出(及其输入to()
方法)。不会有 O
类型的实际值传递给 transformer()
,但编译器将能够从 transforms
推断它。用 transforms
表示 O
参数和函数返回类型比直接用 transforms
参数表示返回类型要容易得多(这将涉及各种类型的查询)。
transforms
对象的 options
属性是一个 mapped type,我们获取 P
的每个属性键 O
并使用其值类型 {{1 }} 来构造 O[P]
/from
方法类型。
对于函数的返回类型,我们首先用一个键为 to
类型,值为 O
的属性来扩充 K
。 (我为此使用了 I
类型函数。)然后我们通过获取每个属性并用 AddProp
包装它来映射增强类型。
这就是打字。至于实现,几乎不可能让编译器验证它是否符合如此复杂的调用签名。所以最好的方法是非常小心地正确实现它,并使用等效的 type assertions 来抑制编译器错误。在这种情况下,我倾向于使用带有所需类型的单个调用签名的 overloaded function,并且其实现签名明显宽松(在必要时使用 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;
}
看起来不错!