簡體   English   中英

如何在 typescript 中限制 class 的允許屬性

[英]How to restrict the allowed properties of a class in typescript

我需要限制 class 可以擁有的屬性名稱和類型。 我發現這樣做的唯一方法是

type ForbiddenKeys = "x"|"y"|"z"

type Allowed = string|number|boolean|Set<string|number>|Array<string|number>|null|undefined
type AllowedObject = { [s:string]: Allowed | AllowedObject } & { [F in ForbiddenKeys]?: never }

class A {
    [s:string]: Allowed | AllowedObject 
    private x?: never
    private y?: never
    private z?: never
    static scan(): string {
        return "DUMMY static method return value"
    }
    save(): void {
        // DUMMY empty method
    }
}

此 class 將用作抽象 class 以使編譯器了解擴展類將具有的隱藏方法和禁止的屬性名稱。 擴展類實際上將有一個裝飾器應用於它們的方法的真正邏輯所在的位置

function addMethods<T extends { new (...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
        static scan() {
            // Real logic goes here
            return "scan() static method got executed."
        }
        save() {
            console.log(`${JSON.stringify(this)} has been saved`)
            // REAL method logic goes here
        }
    }
}

@addMethods
class B extends A { // <-- Only properties definitions go here while methods are added by the decorator.
    x?: string // <-- Error as needed. We don't want "x" here
    a?: string
    b?: number
    c?: {
        d?: boolean
        //y?: string // <-- Error as needed. We don't want "y" here
    }
}

遵循示例用法

const b = new B()
b.a = "A"
b.b = 0
b.save() // <-- return value: void. Compiler must be aware of this. Decorator logic gets executed.
const scan = B.scan() // <-- const scan: string. Compiler must be aware of this.
console.log(scan) // <-- Prints: "scan() static method got executed."

這一直有效,直到我需要使用子 class 的屬性名稱。 即使是對 B 的屬性進行迭代的簡單類型,也不會按預期運行,因為keyof T包含[s:string]

type Props<T> = {
    [K in keyof T]?: T[K]
}

const props: Props<B> = {
    a: "abcd",
    b: 0,
    anyProperty: "anything" // <-- No error. I need an error here.
}

以下類型是我真正需要的更接近(簡化)的示例。 它是一種將禁止屬性添加到 class 的每個鍵的類型,對其嵌套對象也是如此

type AddProps<T> = {
    [K in keyof T]?: T[K] extends Allowed ? { 
        [F in ForbiddenKeys]?: T[K] 
    } : T[K] extends (AllowedObject|undefined) ? AddProps<T[K]> : never
}

function addProps<T>(propsToAdd: AddProps<T>) {
    return propsToAdd
}

addProps<B>({  // <-- We don't want errors here.
    a: { x: "some string" }, 
    b: { y: 0 },
    c: { 
        d: {
            z: true
        }
    }
})

這無法完成,因為keyof T包括[s:string]而不僅僅是我在class B中聲明的屬性

問題

有沒有辦法實現我所追求的? 游樂場鏈接

這里的主要問題是 TypeScript 中沒有特定的 object 類型,它在不添加字符串索引簽名的情況下限制值類型。 如果我想說 object 只能具有boolean值屬性,那么我唯一可用的特定 object 類型是type Boo = {[k: string]: boolean} 但是keyof Boo將是string ,這是您不想要的。

由於我們不能真正將AllowedObject寫為特定的 object 類型,我們可以嘗試將其寫為通用約束 也就是說, VerifyAllowed<T>檢查候選類型T是否被允許。 如果T extends VerifyAllowed<T> ,則允許,否則不允許。

這是一種可能的實現:

type VerifyAllowed<T> = T extends Allowed ? T :
    T extends object ? {
        [K in keyof T]: K extends ForbiddenKeys ? never : VerifyAllowed<T[K]>
    } : never

如果TAllowed ,則VerifyAllowed<T>將解析為T (因此T extends VerifyAllowed<T>將為真)。 否則,如果Tobject類型,我們將 map每個屬性T[K]VerifyAllowed<T[K]>除非密鑰KForbiddenKeys之一,在這種情況下我們從never到它。 因此,如果沒有任何鍵被禁止,則T extends VerifyAllowed<T>如果所有屬性都允許,則成功,否則失敗。 如果甚至禁止一個鍵,則該屬性將映射為never ,然后T extends VerifyAllowed<T>將為 false。 最后,如果T既不是Allowed ,也不是object ,那么這是我們不想要的一些原語(比如symbol ),所以我們只返回never ,這樣T extends VerifyAllowed<T>就會是假的。


好的,那么我們如何使用它呢? 如果您使用class定義,一種方法是將其放入implements子句中以正確方式捕獲任何不符合要求的class 這不是必需的,但如果沒有它,您只會在第一次嘗試將 class 實例傳遞給某物時捕獲錯誤。 無論如何,它看起來像這樣:

class A implements VerifyAllowed<A> {
    static scan(): string {
        return "DUMMY static method return value"
    }
    save(): void {
    }
}    

@addMethods
class BadB extends A implements VerifyAllowed<BadB> {
    a?: string
    b?: number
    c?: { // error! // Types of property 'y' are incompatible
        d: boolean
        y: string
    }
}

糟糕,我們犯了一個錯誤,把y放在那里。 讓我們刪除它:

@addMethods
class B extends A implements VerifyAllowed<B> { // okay
    a?: string
    b?: number
    c?: {
        d: boolean
    }
}

無論我們是否在class聲明中使用implements VerifyAllowed<> ,我們仍然可以通過使任何接受“允許”事物泛型的 function 來捕捉錯誤。 例如:

function acceptOnlyAllowedThings<T>(t: VerifyAllowed<T>) {

}

const badB = new BadB();
const b = new B();

acceptOnlyAllowedThings(badB); // error! c.y is bad
acceptOnlyAllowedThings(b); // okay

現在我們已經將約束放在那里,我們可以將Props<T>定義為Partial<T>實用程序類型相同的東西,因為沒有字符串索引簽名會搞砸你:

type Props<T> = Partial<T>; // <-- this is just Partial

const props: Props<B> = {
    a: "abcd",
    b: 0,
    anyProperty: "anything" // error!
}

AddProps<T>也是如此:您可以遞歸地將T轉換為AddProps<T>而不必擔心字符串索引簽名:

type AddProps<T> = T extends VerifyAllowed<T> ? {
    [K in keyof T]?: T[K] extends Allowed ? {
        [F in ForbiddenKeys]?: T[K]
    } : AddProps<T[K]>
} : never;

const test: AddProps<B> = {
    a: { x: "some string" },
    b: { y: 0 },
    c: {
        d: { z: true }
    }
}

看起來不錯!

Playground 代碼鏈接

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM