我应该如何在 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表示一个stringliteral typecreateFunctionObject的输出会是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类型,对于某些AR,那么输出函数类型是(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")

请注意,我必须将 abc 回调参数注释为 {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

看起来不错!

Playground link to code