首页 > 解决方案 > 如何计算打字稿中对象的动态键类型

问题描述

我开始使用 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()为类型numberstring.read()类型stringfrom()

我已经完成了许多关于 SO 的问答,但都没有成功。

标签: typescripttype-inference

解决方案


这是我给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类型中,编译器将忘记它。

  • Ivalue参数的类型,是每个变换from()方法的输入(以及它的to()方法的输出)。

  • O extends object是一个对象,其属性是对象的键transforms,其值是相应变换from()方法的输出(及其to()方法的输入)。不会有实际的类型值O传入transformer(),但编译器将能够从 中推断出它transforms。用参数表示transforms参数和函数返回类型Otransforms直接用参数表示返回类型要容易得多(这将涉及各种类型查询)。

对象的transforms属性options是一个映射类型,我们获取每个属性键PO使用其值类型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"

看起来挺好的!

Playground 代码链接


推荐阅读