使用通用对象类型并集缩小类型

时间:2019-01-13 16:02:23

标签: typescript

以下类型在事件系统中用于对象更改。

我在根据某些条件缩小对象类型时遇到麻烦。例如,当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
}

1 个答案:

答案 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
}