首页 > 解决方案 > 在 TypeScript 中,我们可以使用“运行时”键来定义新类型吗?

问题描述

更好地解释这个作为一个例子:

class ModuleOptions {
  key1?: string;
  key2?: string;

  keyA?: string;
  keyB?: string;
}

class Module {
  static options: ModuleOptions = {
    key1: 'key1',
    key2: 'key2',

    keyA: 'keyA',
    keyB: 'keyB'
  };

  static create(options: ModuleOptions) {
    Object.assign(Module.options, options);
  }
}


const myModule = Module.create({
  key1: 'foo'
});

// Now Module.options static field has "foo" as key1...
// Could we constrait fooArgs to have a property named "foo" of type string?
function foo(fooArgs: NewType) {
  // Do stuff
}

是否可以fooArgs(即NewType)只接受'foo'作为键,并为其定义相同的类型(string)?

这不起作用(即使变得简单):

class NewType {
  [k in keyof [ModuleOptions.options]]: string;
}

计算属性名称必须是“字符串”、“数字”、“符号”或“任意”类型。

标签: typescript

解决方案


你可能会得到你想要的东西,但肯定有痛点。第一个主要问题是 TypeScript 不允许您任意改变现有值的类型。因此,如果名为的变量foo具有 type string,则不能将其更改为number以后。在您的情况下,如果Module.options.key1编译器知道它是string-literal type "key1",那么您以后不能将其类型更改为 string-literal 类型"foo"。编译器会期望它"key1"永远存在,即使在您调用Module.create(). 有几种方法可以解决这个问题。一种是使用类型保护缩小范围值的类型(值的类型可以更具体,但不能随意改变),还有一种是用新的变量来表示新类型的值。

这是使用后一种方法的可能解决方案...返回具有不同类型create的新版本:Module

module Module {

  type Module = typeof Module; // handy type alias

  const defaultOptions = {
    key1: 'key1' as 'key1',
    key2: 'key2' as 'key2',
    keyA: 'keyA' as 'keyA',
    keyB: 'keyB' as 'keyB',
  }; // use type assertions to narrow to string literals

  type DefaultOptions = typeof defaultOptions; // handy type alias

  export const options: Record<keyof DefaultOptions, string> = defaultOptions;

  // represent overwriting properties from T with properties from U
  type Assign<T, U> = { 
    [K in keyof T | keyof U]: K extends keyof U ? U[K] : K extends keyof T ? T[K] : never 
  };

  export function create<T>(
    o: T
  ): { [K in keyof Module]: 'options' extends K ? Assign<DefaultOptions, T> : Module[K] } {
    Object.assign(options, o);
    return Module as any; // can't represent type mutation, use type assertion here
  }

}

它使用条件类型来表示options调用create. 让我们看看它是如何工作的:

const almostMyModule = Module.create({
  key1: 'foo'
});
almostMyModule.options.key2; // "key2"
almostMyModule.options.key1; // string? oops

糟糕,传递给的参数的类型create()被推断为{key1: string},而不是{key1: "foo"}。这是另一个痛点。有一些方法可以使推理正确,但现在我只使用类型断言来缩小它:

const myModule = Module.create({
  key1: 'foo' as 'foo'
});
myModule.options.key1; // 'foo', that's better
myModule.options.key2; // 'key2'

现在myModule.options.key1编译器知道它是"foo",我们可以制作NewType你想要的:

type ValueOf<T> = T[keyof T];
type NewType = { [K in ValueOf<typeof myModule.options>]: string };

检查NewType给我们:

// type NewType = {
//   foo: string;
//   key2: string;
//   keyA: string;
//   keyB: string;
// }

你可以使用它:

function foo(fooArgs: NewType) {
  fooArgs.foo // okay
}

希望有帮助。祝你好运!


推荐阅读