如何计算打字稿中对象的动态键类型

时间:2021-07-30 03:41:34

标签: typescript type-inference

我刚开始使用 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 的问答,但都没有成功。

1 个答案:

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

  • Ivalue 参数的类型,它是每个转换的 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;

}

看起来不错!

Playground link to code