以下类型在事件系统中用于对象更改。
我在根据某些条件缩小对象类型时遇到麻烦。例如,当prop
属性为null时,我们肯定知道deleted
将为false。
失败的案例如下所示:(playground link)
declare const change: Change<{ a: number; b: string }>
if (change.prop === 'a') {
change.prop // OK
change.newValue // Expected to be "number"
}
if (change.prop == null) {
change.prop // Expected to be "null"
change.deleted // Expected to be "false"
change.newValue // Expected to be "{a: number; b: string}"
}
type Change<T = any> =
| RootChange<T>
| (T extends object ? NestedChange<T> : never)
type RootChange<T> = IChange & {
prop: null
oldValue: T
newValue: T
deleted: false
}
type NestedChange<T extends object = any, P extends keyof T = keyof T> =
| (IChange & {
prop: P
oldValue: T[P]
newValue: T[P]
deleted: false
})
| (IChange & {
prop: P
oldValue: T[P]
newValue: undefined
deleted: true
})
interface IChange {
/** The property being changed. When null, this change is for the root value. */
prop: keyof any | null
/** The previous value */
oldValue: unknown
/** The next value */
newValue: unknown
/** Whether the property has been deleted */
deleted: boolean
}
答案 0 :(得分:1)
问题出在NestedChange
上。为{a: number, b: string}
定义类型的方式等效于:
type NestedChange<{a: number, b: string}> =
| (IChange & {
prop: "a" | "b"
newValue: number | string
deleted: false
})
| (IChange & {
prop: "a" | "b"
newValue: undefined
deleted: true
})
因此,属性类型和属性名称之间没有关系,就编译器而言,prop:"a"
可以与string
配对。
您想要一个看起来更像这样的联合:
{ prop: "a"; newValue: number; deleted: false; } |
{ prop: "a"; newValue: undefined; deleted: true; } |
{ prop: "b"; newValue: string; deleted: false; } |
{ prop: "b"; newValue: undefined; deleted: true; }
您可以使用联合类型的分布行为来创建这样的联合(读here)。这意味着,如果我们有一个包含T
的键的并集的裸类型参数,我们可以遍历键并在每个键上应用类型转换,并得到一个包含应用于每个键的转换的并集。
要引入新的类型参数并在其上进行分布,我们使用两种条件类型:keyof T extends infer P ? P extends any ? ... : never: never
。在两种类型的条件都不重要的情况下,我们使用第一个条件(keyof T extends infer P
)引入新的类型参数P
,并使用第二个条件(P extends any
)触发分布行为
declare const change: Change<{ a: number; b: string }>
if (change.prop === 'a') {
change.prop // OK
change.newValue // is number | undefined
if (change.deleted) {
change.newValue // undefined
} else {
change.newValue // number
}
}
if (change.prop == null) {
change.prop // is "null"
change.deleted // is "false"
change.newValue // is "{a: number; b: string}"
}
type Change<T = any> =
| RootChange<T>
| (T extends object ? NestedChange<T> : never)
type RootChange<T> = IChange & {
prop: null
newValue: T
deleted: false
}
type NestedChange<T extends object> = keyof T extends infer P ?
P extends any ?
(IChange & {
prop: P
newValue: T[P]
deleted: false
})
| (IChange & {
prop: P
newValue: undefined
deleted: true
})
: never : never;
interface IChange {
/** The property being changed. When null, this change is for the root value. */
prop: keyof any | null
/** The previous value */
oldValue: unknown
/** The next value */
newValue: unknown
/** Whether the property has been deleted */
deleted: boolean
}