首页 > 解决方案 > TypeScript:构造函数中的位置或命名参数

问题描述

我有一个类目前采用 7+ 位置参数。

class User {
  constructor (param1, param2, param3, …etc) {
    // …
  }
}

我想通过选项对象将其转换为命名参数。

type UserOptions = {
  param1: string
  // …
}

class User {
  constructor ({ param1, param2, param3, …etc } = {}: UserOptions) {
    // …
  }
}

这很好,除了现在需要重新设计很多测试来更改签名,所以我想同时支持命名参数和位置参数。

我可以使代码同时支持两者,但我不确定如何在不写出所有类型两次的情况下将类型放入其中。理想情况下, my 中的类型列表UserOptions将按照它们在中定义的顺序成为位置参数UserOptions

有没有办法做这样的事情?

type UserOptions = {
  param1: string
  // …
}

type ToPositionalParameters<T> = [
  // ??? something like ...Object.values(T)
  // and somehow getting the keys as the positional argument names???
]

type PositionalUserOptions = ToPositionalParameters<UserOptions>

class User {
  constructor (...args: PositionalUserOptions);
  constructor (options: UserOptions);
  constructor (...args: [UserOptions] | PositionalUserOptions) { 
    // …
  }
}

还会考虑以另一种方式工作的解决方案,即命名的位置参数,也许这更容易?

标签: javascripttypescriptclassconstructorconstructor-overloading

解决方案


这里有几件事阻碍了你。

首先是对象类型中属性的顺序目前在 TypeScript 中是不可观察的,因为它们不会影响可分配性。{a: string, b: number}例如,类型和{b: number, a: string}类型系统中没有区别。您可以执行一些肮脏的技巧来从编译器中提取信息以查看它是否将键表示为["a", "b"]vs ["b", "a"],但所做的只是在编译时为您提供一些排序;当您从上到下阅读类型声明时,不能保证是有序的……哎呀,甚至不能保证每次编译时都是相同的顺序!微软/TypeScript#17944对于一个开放的问题,要求有一个一致的排序。请参阅microsoft/TypeScript#42178以获取看似无关的代码更改示例,该更改会改变预期的顺序。目前,我们不能以任何一致的方式自动将对象类型转换为有序元组。

第二个是函数类型中的参数名称故意不能用作字符串文字类型。它们是不可观察的,原因与对象属性排序类似:它们不影响可分配性。(foo: string) => void例如,函数类型和(bar: string) => void类型系统中没有区别 。我什至认为没有任何技巧可以暴露这些细节。在类型系统中,函数参数名称仅用作文档或 IntelliSense。您可以在命名函数参数和标记元组元素之间进行转换,仅此而已。请参阅microsoft/TypeScript#28259 中的此评论解释我们不能在 TypeScript 中使用调用签名参数名称或元组标签作为字符串。目前,我们不能自动将带标签的元组转换为对象类型,其中对象的键对应于元组的标签。


如果我们不尝试将对象转换为元组或将元组转换为对象,而是提供足够的信息来完成这两个问题,那么您可以回避这两个问题:

const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [string, number, boolean];

userOptionKeysUserOptions对象中所需键的显式有序列表......与我们手动创建的元组中的顺序相同PositionalUserOptions。现在我们有了键名,我们可以构建UserOptions

type UserOptions = { [I in Exclude<keyof PositionalUserOptions, keyof any[]> as
  typeof userOptionKeys[I]]: PositionalUserOptions[I] }
/* type UserOptions = {
    param1: string;
    param2: number;
    thirdOne: boolean;
} */

当我们这样做的时候,我们可以编写一个函数来将一个类型的元组转换为一个类型PositionalUserOptions的对象UserOptions(使用any类型断言来使编译器不必尝试验证它,而这是它不容易做到的):

function positionalToObj(opts: PositionalUserOptions): UserOptions {
  return opts.reduce((acc, v, i) => (acc[userOptionKeys[i]] = v, acc), {} as any)
}

现在我们可以编写User该类,positionalToObj在构造函数的实现中使用来规范化事物:

class User {
  constructor(...args: PositionalUserOptions);
  constructor(options: UserOptions);
  constructor(...args: [UserOptions] | PositionalUserOptions) {
    const opts = args.length === 1 ? args[0] : positionalToObj(args);
    console.log(opts);
  }
}

new User("a", 1, true);
/* {
  "param1": "a",
  "param2": 1,
  "thirdOne": true
}  */

有用!从类型系统和可分配性的角度来看,这是您能做的最好的事情。从文档/IntelliSense 的角度来看,它不是很好。当您调用new User()时,多参数版本的文档将为您提供 和 之类的args_0标签args_1。如果你希望它们是param1and param2,你只需要硬着头皮写出两次参数名称;一次作为字符串文字,再次作为元组标签,因为没有办法将一个转换为另一个:

const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [param1: string, param2: number, thirdOne: boolean];

这值得么?也许……这取决于你。

Playground 代码链接


推荐阅读