代码之家  ›  专栏  ›  技术社区  ›  aleclarson

使用泛型对象类型联合进行类型缩小

  •  1
  • aleclarson  · 技术社区  · 7 年前

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

    我在基于某些条件缩小对象类型方面遇到了问题。例如,当 prop 属性为null,我们确信 deleted

    失败案例如下所示: (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 回复  |  直到 7 年前
        1
  •  1
  •   Titian Cernicova-Dragomir    7 年前

    问题在于 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
    }
    
    推荐文章