映射类型中的 TypeScript 可选参数是错误必需的

问题描述 投票:0回答:1

我正在开发一个 TypeScript 实用程序,它允许使用定义的模式注册事件并发出它们,同时对参数强制执行严格的类型检查。

问题是由架构中定义的可选参数引起的。我希望当一个键被标记为可选时,应该可以完全省略它。但是,当我在事件发射期间省略该键时,TypeScript 仍然会引发错误。

这是我的问题的最小再现:

type ParameType<T> = {
  regexSnippet: string
  parse?: (value: string) => any
  type: T
}

const ParameterTypes = {
  "string": <ParameType<string>>{ regexSnippet: "\\w+" },
  "number": <ParameType<number>>{ regexSnippet: "\\d+", parse: parseInt },
  "string?": <ParameType<undefined | string>>{ regexSnippet: "\\w*" },
  "number?": <ParameType<undefined | number>>{ regexSnippet: "\\d*", parse: parseInt },
} as const

type ParamsTypeMap = typeof ParameterTypes
type ParamsTypeKeys = keyof ParamsTypeMap
type ParamsSchema = { [name: string]: ParamsTypeKeys }
type ParamsSchemaToObject<T extends ParamsSchema> = { [K in keyof T]: ParamsTypeMap[T[K]]["type"] }

type Settings = {
  parameters: ParamsSchema
}

class Test<T extends { [name: string]: Settings } = {}> {
  protected events: T = {} as T

  registerEvent<K extends string, V extends Settings>(
    eventname: K,
    settings: V
  ): asserts this is Test<T & { [keyname in K]: V }> {
    // ...
  }

  emit<K extends keyof T>(
    eventname: K,
    parameters: ParamsSchemaToObject<T[K]["parameters"]>
  ): any {
    // ...
  }
}

const test: Test = new Test()

test.registerEvent("prmtst", { parameters: { a: "number", b: "number?", c: "string" } })
test.emit("prmtst", { a: 1, b: 2, c: "str" }) // OK
test.emit("prmtst", { a: 1, c: "str" })       // Error, but `b` is optional

在上面的示例中,参数

b
被标记为可选 (
number?
),但当我完全省略它时,TypeScript 仍然会抛出错误。

问题
如何修改

ParamsSchemaToObject
类型,以便在发出事件时可以安全地省略
b
等可选键?

typescript
1个回答
0
投票

您正在尝试完成两件不同的事情。


其中之一是,每当您有一个接受

undefined
的属性时,您都希望该属性是 可选。为此,我们可以编写一个实用程序类型
UndefToOptional<T>
,将
{a: string | undefined, b: number}
之类的类型转换为
{a?: string, b: number}
之类的类型。最简单的方法是使用 Partial 实用程序类型使每个属性都是可选的,然后将其与 key-remapped type 进行 intersect ,该类型仅根据需要保留不接受
undefined
的属性:

type UndefToOptional<T> = Partial<T> &
{ [K in keyof T as undefined extends T[K] ? never : K]: T[K] }

type X = UndefToOptional<{ a: string | undefined, b: number }>;
//   ^? type X = Partial<{ a: string | undefined; b: number; }> & { b: number; }

这可能不是您想要的类型,但它确实有效。更复杂的定义是:

type UndefToOptional<T> = { [K in keyof T]:
  (x: undefined extends T[K] ? { [P in K]?: T[K] } : { [P in K]: T[K] }) => void } extends
  Record<keyof T, (x: infer V) => void> ? { [K in keyof V]: V[K] } : never

type X = UndefToOptional<{ a: string | undefined, b: number }>;
//   ^? type X = { a?: string | undefined; b: number; }

可以看到输出类型更漂亮了。我在这里所做的实际上与“将联合类型转换为交集类型”方法相同,在该方法中,我们将一堆单属性对象相交,每个对象要么是部分的,要么是必需的。因此,对于 {a: string | undefined, b: number},我们最终得到

{a?: string} & {b: number}
,然后将它们与
{[K in keyof V]: V[K]}
组合在单个 映射类型 中。 我不知道是否值得准确描述其工作方式和原因。如果您想要更简单的实现,请使用上面的交集。
现在我们可以修改 

ParamsSchemaToObject

以便它使用

UndefToOptional
:
type ParamsSchemaToObject<T extends ParamsSchema> =
  UndefToOptional<{ [K in keyof T]: ParamsTypeMap[T[K]]["type"] }>

第二件事是,您希望 
emit()

方法允许第二个参数是可选的(如果它接受一个

弱类型
对象)。弱对象具有所有可选属性。 TypeScript 并不容易通过开关让参数成为可选参数或必需参数。如果您为函数提供一个元组类型的剩余参数,则可以做到这一点,该参数可以是常规(必需)元组,也可以是带有可选元素的元组。那么让我们定义 WeakToOptionalTuple<T>:
type WeakToOptionalTuple<T> = {} extends T ? [T?] : [T]

弱对象是可以分配空对象
{}

的对象。如果

T
是弱的,则
WeakOptionalTuple
是一个单元素元组,带有
可选
T 元素;否则它是一个带有
required
T 元素的单元素元组。 然后我们可以将
emit()
写为
emit<K extends keyof T>(
  eventname: K, ...[parameters]: WeakToOptionalTuple<
    ParamsSchemaToObject<T[K]["parameters"]>
  >): any {
  // ...
}

其中最后一个参数是名为 
parameters

的解构剩余参数,其类型是可选或必需的元组。


让我们测试一下:

test.registerEvent("str", { parameters: { a: "string?" } }); test.registerEvent("prmtst", { parameters: { a: "number", b: "number?", c: "string" } }); test.emit("prmtst", { a: 1, c: "str" }); // okay test.emit("str", {}); // okay test.emit("str"); // okay test.emit("prmtst"); // error

如果您查看 
test.emit("prmtst", ⋯)

的调用签名,它是

(eventname: "prmtst", parameters: { a: number; b?: number; c: string;}): any
,而
test.emit("str", ⋯)
的调用签名是
(eventname: "str", parameters?: { a?: string }): any
,因此您可以看到,在您关心的情况下,属性和参数是可选的.

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.