TypeScript:构造函数中的位置或命名参数

2024-02-14

我有一个类当前采用 7 个以上的位置参数。

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

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

type UserOptions = {
  param1: string
  // …
}

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

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

我可以使代码支持两者,但我不确定如何在不写出所有类型两次的情况下获取其中的类型。理想情况下我的类型列表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) { 
    // …
  }
}

还会考虑一种相反的解决方案,即命名的位置参数,也许这更容易?


这里有一些东西阻碍了你。

第一个是对象类型中属性的顺序当前是不可观察的在 TypeScript 中,因为它们不影响可分配性。例如,类型之间没有区别{a: string, b: number} and {b: number, a: string}在类型系统中。有肮脏的伎俩 https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type您可以执行从编译器中提取信息以查看它是否将键表示为["a", "b"] vs ["b", "a"],但所做的只是给你some编译时排序;当您从上到下阅读类型声明时,并不能保证它是有序的......哎呀,它甚至不能保证是the same每次编译时都要订购!看微软/TypeScript#17944 https://github.com/microsoft/TypeScript/issues/17944对于一个悬而未决的问题,要求有一个一致的顺序。并看到微软/TypeScript#42178 https://github.com/microsoft/TypeScript/issues/42178一个看似无关的代码更改的示例,它改变了预期的顺序。目前,我们无法以任何一致的方式自动将对象类型转换为有序元组。

第二个是函数类型中的参数名称是故意的无法使用 as 字符串文字类型 https://www.typescriptlang.org/docs/handbook/literal-types.html#string-literal-types。它们是不可观察的,原因与对象属性排序类似:它们不影响可分配性。例如,函数类型之间没有区别(foo: string) => void and (bar: string) => void在类型系统中。我什至不认为有任何技巧可以暴露这些细节。在类型系统中,函数参数名称仅用作文档或 IntelliSense。您可以在命名函数参数和标记元组元素 https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#labeled-tuple-elements,但仅此而已。看microsoft/TypeScript#28259 中的此评论 https://github.com/microsoft/TypeScript/issues/28259#issuecomment-671741399解释我们不能在 TypeScript 中使用调用签名参数名称或元组标签作为字符串。目前,我们无法自动将带标签的元组转换为对象类型,其中对象的键对应于元组的标签。


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

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

Here userOptionKeys是所需键的显式有序列表UserOptions对象...与我们手动创建的顺序相同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
}  */

有用!从类型系统和可分配性的角度来看,这是您能做的最好的事情。从文档/智能感知的角度来看,这并不是很好。你打电话时new User(),多参数版本的文档将为您提供类似的标签args_0 and args_1。如果你想要那些param1 and param2,你只需要硬着头皮将参数名称写出两次即可;一次作为字符串文字,再次作为元组标签,因为无法将一个转换为另一个:

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

TypeScript:构造函数中的位置或命名参数 的相关文章

随机推荐