我应该如何在 Typescript 中定义一个动态类型,它应该保留用户输入的实际类型
How should I define a dynamic type in Typescript which should preserve the actual type entered by user
我正在创建一个函数,它接受参数中的某些对象,其接口如下 {[someKey]: (data: any) => any}
type GenericRecord<value> = Record<string, value>
function createFunctionObject (funcObject: GenericRecord<(someParam: GenericRecord<any>, randomKey: string) => any>) {
return Object.fromEntries(
Object.entries(funcObject).map(([key, fuc]) => [
key,
(param: GenericRecord<any>) => fuc(param[randomKey])
])
);
}
签名保证函数 createFunctionObject
将接受一个函数对象并将 return 一个函数对象。
问题?
我想保留 returned 对象将与传递的对象具有相同的键,并且作为 arg 传递的函数的 return 值将与 return 函数的值 return 由 createFunctionObject
编辑。
const {x, y, na } = createFunctionObject(
{ x: (param: GenericRecord<any>): number => state.x, y: (param:
GenericRecord<any>): string => state.y, z: (param:
GenericRecord<any>): string => state.z }, 'someKey'
);
这意味着我将在 return {x, y, z}
中获得 3 个函数,并且使用 x, y, na
进行解构应该会引发错误。此外,它应该保留 x 将 return 一个数字和 y,z 应该 return 一个字符串。但是我得到的 createFunctionObject
的 return 类型签名是 { [k: string]: (state: GenericRecord<any>) => any; }
函数示例:
function createFunctionObject (funcObject, randomKey) {
return Object.fromEntries(
Object.entries(funcObject).map(([key, fuc]) => [
key,
(param) => fuc(param[randomKey])
])
);
}
const {a, b, c} = createFunctionObject({ a: (arg) => arg.a, b: (arg) => arg.b, c: (arg) => arg.c }, 'x')
console.log(a({x: {a: 20}})) // 20
console.log(b({x: {b: 30}})) // 30
首先让我们描述一下 createFunctionObject
应该在类型级别做什么,而不用担心实现。给定类型 T
的输入 funcObject
和类型 K
的输入 randomKey
,其中 T
表示一个对象,其值是一个参数的函数,并且 K
表示一个string
literal type,createFunctionObject
的输出会是FunctionObject<T, K>
类型,定义为:
type FunctionObject<T extends Record<keyof T, (arg: any) => any>, K extends string> =
{ [P in keyof T]:
T[P] extends (arg: infer A) => infer R ?
(arg: { [Q in K]: A }) => R : never
};
观察到 FunctionObject<T, K>
是一个 mapped type,我们将 T
的每个函数值属性转换为一个新的函数类型。如果输入函数类型是(arg: A) => R
类型,对于某些A
和R
,那么输出函数类型是(arg: { [Q in K]: A}) => R
类型。因此,如果输入函数需要一个类型为 A
的参数,那么输出函数需要一个在键 K
处具有类型 A
的 属性 对象的参数。
所以 createFunctionObject()
的调用签名应该是这样的:
function createFunctionObject<
T extends Record<keyof T, (arg: any) => any>,
K extends string
>(
funcObject: T,
randomKey: K
): { [P in keyof T]:
T[P] extends (arg: infer A) => infer R ?
(arg: { [Q in K]: A }) => R : never
};
现在,虽然我们可以在类型级别表达此转换,但实际上不可能让编译器验证您的实现是否符合此类型签名。 Object.fromEntries()
、Object.entries()
和 Array.prototype.map()
的标准库类型不够详细,无法做到这一点,即使我们为它们添加自己的类型签名,我们也会发现编译器将无法以避免编译器错误的方式组合它们。所以我们甚至都不要尝试这样做。
相反,我们只会告诉编译器不要担心过多地验证实现。一种方法是在实现中使用 type assertions:
function createFunctionObject<
T extends Record<keyof T, (arg: any) => any>,
K extends string
>(
funcObject: T,
randomKey: K
): FunctionObject<T, K> {
return Object.fromEntries(
Object.entries(funcObject).map(([key, fuc]) => [
key,
(param: Record<string, any>) => (fuc as Function)(param[randomKey])
]) // assert here
) as any; // assert here
}
但就个人而言,在这种情况下,我喜欢使用带有单个调用签名的 overloaded function。编译器检查重载函数的实现比检查常规函数的实现更宽松,这对我们来说就足够了:
// call signature
function createFunctionObject<
T extends Record<keyof T, (arg: any) => any>,
K extends string
>(
funcObject: T,
randomKey: K
): FunctionObject<T, K>;
// implementation (checked more loosely)
function createFunctionObject(
funcObject: Record<string, Function>,
randomKey: string
) {
return Object.fromEntries(
Object.entries(funcObject).map(([key, fuc]) => [
key,
(param: Record<string, any>) => fuc(param[randomKey])
])
);
}
两个版本都可以。
让我们确保调用站点的行为符合您的预期:
const { a, b, c } = createFunctionObject({
a: (arg: { a: number }) => arg.a,
b: (arg: { b: number }) => arg.b,
c: (arg: { c: number }) => arg.c
}, "randomKey")
请注意,我必须将 a
、b
和 c
回调参数注释为 {a: number}
、{b: number}
和 {c: number}
;如果你让它们没有注释为 a => arg.a
,编译器将不知道如何推断它们,这最终会退回到 any
。这是usually a mistake,所以最好在编译器无法推断的时候注释它们。
无论如何,您可以看到生成的函数是可调用的并且具有正确的类型签名:
/* const a: (arg: {
randomKey: {
a: number;
};
}) => number */
console.log(a({ randomKey: { a: 20 } })) // 20
/* const b: (arg: {
randomKey: {
b: number;
};
}) => number */
console.log(b({ randomKey: { b: 30 } })) // 30
看起来不错!
我正在创建一个函数,它接受参数中的某些对象,其接口如下 {[someKey]: (data: any) => any}
type GenericRecord<value> = Record<string, value>
function createFunctionObject (funcObject: GenericRecord<(someParam: GenericRecord<any>, randomKey: string) => any>) {
return Object.fromEntries(
Object.entries(funcObject).map(([key, fuc]) => [
key,
(param: GenericRecord<any>) => fuc(param[randomKey])
])
);
}
签名保证函数 createFunctionObject
将接受一个函数对象并将 return 一个函数对象。
问题?
我想保留 returned 对象将与传递的对象具有相同的键,并且作为 arg 传递的函数的 return 值将与 return 函数的值 return 由 createFunctionObject
编辑。
const {x, y, na } = createFunctionObject(
{ x: (param: GenericRecord<any>): number => state.x, y: (param:
GenericRecord<any>): string => state.y, z: (param:
GenericRecord<any>): string => state.z }, 'someKey'
);
这意味着我将在 return {x, y, z}
中获得 3 个函数,并且使用 x, y, na
进行解构应该会引发错误。此外,它应该保留 x 将 return 一个数字和 y,z 应该 return 一个字符串。但是我得到的 createFunctionObject
的 return 类型签名是 { [k: string]: (state: GenericRecord<any>) => any; }
函数示例:
function createFunctionObject (funcObject, randomKey) {
return Object.fromEntries(
Object.entries(funcObject).map(([key, fuc]) => [
key,
(param) => fuc(param[randomKey])
])
);
}
const {a, b, c} = createFunctionObject({ a: (arg) => arg.a, b: (arg) => arg.b, c: (arg) => arg.c }, 'x')
console.log(a({x: {a: 20}})) // 20
console.log(b({x: {b: 30}})) // 30
首先让我们描述一下 createFunctionObject
应该在类型级别做什么,而不用担心实现。给定类型 T
的输入 funcObject
和类型 K
的输入 randomKey
,其中 T
表示一个对象,其值是一个参数的函数,并且 K
表示一个string
literal type,createFunctionObject
的输出会是FunctionObject<T, K>
类型,定义为:
type FunctionObject<T extends Record<keyof T, (arg: any) => any>, K extends string> =
{ [P in keyof T]:
T[P] extends (arg: infer A) => infer R ?
(arg: { [Q in K]: A }) => R : never
};
观察到 FunctionObject<T, K>
是一个 mapped type,我们将 T
的每个函数值属性转换为一个新的函数类型。如果输入函数类型是(arg: A) => R
类型,对于某些A
和R
,那么输出函数类型是(arg: { [Q in K]: A}) => R
类型。因此,如果输入函数需要一个类型为 A
的参数,那么输出函数需要一个在键 K
处具有类型 A
的 属性 对象的参数。
所以 createFunctionObject()
的调用签名应该是这样的:
function createFunctionObject<
T extends Record<keyof T, (arg: any) => any>,
K extends string
>(
funcObject: T,
randomKey: K
): { [P in keyof T]:
T[P] extends (arg: infer A) => infer R ?
(arg: { [Q in K]: A }) => R : never
};
现在,虽然我们可以在类型级别表达此转换,但实际上不可能让编译器验证您的实现是否符合此类型签名。 Object.fromEntries()
、Object.entries()
和 Array.prototype.map()
的标准库类型不够详细,无法做到这一点,即使我们为它们添加自己的类型签名,我们也会发现编译器将无法以避免编译器错误的方式组合它们。所以我们甚至都不要尝试这样做。
相反,我们只会告诉编译器不要担心过多地验证实现。一种方法是在实现中使用 type assertions:
function createFunctionObject<
T extends Record<keyof T, (arg: any) => any>,
K extends string
>(
funcObject: T,
randomKey: K
): FunctionObject<T, K> {
return Object.fromEntries(
Object.entries(funcObject).map(([key, fuc]) => [
key,
(param: Record<string, any>) => (fuc as Function)(param[randomKey])
]) // assert here
) as any; // assert here
}
但就个人而言,在这种情况下,我喜欢使用带有单个调用签名的 overloaded function。编译器检查重载函数的实现比检查常规函数的实现更宽松,这对我们来说就足够了:
// call signature
function createFunctionObject<
T extends Record<keyof T, (arg: any) => any>,
K extends string
>(
funcObject: T,
randomKey: K
): FunctionObject<T, K>;
// implementation (checked more loosely)
function createFunctionObject(
funcObject: Record<string, Function>,
randomKey: string
) {
return Object.fromEntries(
Object.entries(funcObject).map(([key, fuc]) => [
key,
(param: Record<string, any>) => fuc(param[randomKey])
])
);
}
两个版本都可以。
让我们确保调用站点的行为符合您的预期:
const { a, b, c } = createFunctionObject({
a: (arg: { a: number }) => arg.a,
b: (arg: { b: number }) => arg.b,
c: (arg: { c: number }) => arg.c
}, "randomKey")
请注意,我必须将 a
、b
和 c
回调参数注释为 {a: number}
、{b: number}
和 {c: number}
;如果你让它们没有注释为 a => arg.a
,编译器将不知道如何推断它们,这最终会退回到 any
。这是usually a mistake,所以最好在编译器无法推断的时候注释它们。
无论如何,您可以看到生成的函数是可调用的并且具有正确的类型签名:
/* const a: (arg: {
randomKey: {
a: number;
};
}) => number */
console.log(a({ randomKey: { a: 20 } })) // 20
/* const b: (arg: {
randomKey: {
b: number;
};
}) => number */
console.log(b({ randomKey: { b: 30 } })) // 30
看起来不错!