【问题标题】:How do I compute dynamic key types for an object in typescript如何计算打字稿中对象的动态键类型
【发布时间】:2021-07-30 03:41:34
【问题描述】:

我刚开始使用 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 可以基于 from() 转换将 number.read() 推断为 number 类型和 string.read() 类型 string 等。

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

【问题讨论】:

  • 您能否为您的transformer 函数提供所需输入和输出类型的示例?

标签: typescript type-inference


【解决方案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 stringoptions 对象的name 字段的类型。此处需要此类型参数,以便您可以跟踪nameliteral 字符串值。否则,如果您将其保留为 string,就像您的 Options 类型一样,编译器将忘记它。

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

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

options 对象的transforms 属性是mapped type,其中我们获取O 的每个属性键P,并使用其值类型O[P] 来构造from/to方法类型。

对于函数的返回类型,我们首先在O 上增加一个属性,其键类型为K,值类型为I。 (我为此使用了AddProp 类型函数。)然后我们通过获取每个属性并用IO&lt;&gt; 包装它来映射该增强类型。


这就是打字。至于实现,您几乎不可能让编译器验证它是否符合如此复杂的调用签名。因此,最好的方法是非常小心地正确实现它,并使用 type assertions 的等效项来抑制编译器错误。在这种情况下,我倾向于使用带有所需类型的单个调用签名的overloaded function,并且其实现签名明显更宽松(在必要时大量使用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 link to code

【讨论】:

猜你喜欢
  • 2020-03-09
  • 1970-01-01
  • 1970-01-01
  • 2021-12-24
  • 2023-02-26
  • 1970-01-01
  • 2022-01-25
  • 2019-01-02
  • 2019-08-23
相关资源
最近更新 更多