我正在开发一个 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
等可选键?
您正在尝试完成两件不同的事情。
其中之一是,每当您有一个接受
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 代码链接