简体   繁体   English

如何在 typescript 中限制 class 的允许属性

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

I need to restrict the properties names and the types a class can have.我需要限制 class 可以拥有的属性名称和类型。 The only way I have found to do this is the following我发现这样做的唯一方法是

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

this class will be used as an abstract class to make the compiler aware of hidden methods and forbidden property names that extending classes will have.此 class 将用作抽象 class 以使编译器了解扩展类将具有的隐藏方法和禁止的属性名称。 The extending classes, will in fact have a decorator applied to them where the real logic of the methods resided扩展类实际上将有一个装饰器应用于它们的方法的真正逻辑所在的位置

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

Follows an example usage遵循示例用法

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."

This works until I need to work with the property names of the child class.这一直有效,直到我需要使用子 class 的属性名称。 Even a simple type which iterates over the properties of B, will not behave as desired because keyof T includes [s:string]即使是对 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.
}

The following type is a closer (simplified) example of what I do really need.以下类型是我真正需要的更接近(简化)的示例。 It is a type which adds the forbidden properties to each key of the class and so does with its nested objects它是一种将禁止属性添加到 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
        }
    }
})

This cannot be done, because keyof T includes [s:string] and not only the properties I declared in class B这无法完成,因为keyof T包括[s:string]而不仅仅是我在class B中声明的属性

Question问题

Is there a way to achieve what I am after?有没有办法实现我所追求的? Playground link 游乐场链接

The main issue here is that there is no specific object type in TypeScript which constrains value types without adding a string index signature .这里的主要问题是 TypeScript 中没有特定的 object 类型,它在不添加字符串索引签名的情况下限制值类型。 If I want to say that an object can only have, say, boolean -valued properties, then the only specific object type available to me is type Boo = {[k: string]: boolean} .如果我想说 object 只能具有boolean值属性,那么我唯一可用的特定 object 类型是type Boo = {[k: string]: boolean} But keyof Boo will be string , which you don't want.但是keyof Boo将是string ,这是您不想要的。

Since we can't really write AllowedObject as a specific object type, we can try writing it as a generic constraint .由于我们不能真正将AllowedObject写为特定的 object 类型,我们可以尝试将其写为通用约束 That is, VerifyAllowed<T> checks a candidate type T for whether it is allowed or not.也就是说, VerifyAllowed<T>检查候选类型T是否被允许。 If T extends VerifyAllowed<T> , then it is allowed, otherwise it is not.如果T extends VerifyAllowed<T> ,则允许,否则不允许。

Here's one possible implementation of that:这是一种可能的实现:

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

If T is Allowed , then VerifyAllowed<T> will resolve to just T (and thus T extends VerifyAllowed<T> will be true).如果TAllowed ,则VerifyAllowed<T>将解析为T (因此T extends VerifyAllowed<T>将为真)。 Otherwise, if T is an object type, we map each property T[K] to VerifyAllowed<T[K]> unless the key K is one of the ForbiddenKeys in which case we map it to never .否则,如果Tobject类型,我们将 map每个属性T[K]VerifyAllowed<T[K]>除非密钥KForbiddenKeys之一,在这种情况下我们从never到它。 So if none of the keys are forbidden, then T extends VerifyAllowed<T> succeeds if all the properties are allowable, and fails otherwise.因此,如果没有任何键被禁止,则T extends VerifyAllowed<T>如果所有属性都允许,则成功,否则失败。 If even one key is forbidden, then that property is mapped to never and then T extends VerifyAllowed<T> will be false.如果甚至禁止一个键,则该属性将映射为never ,然后T extends VerifyAllowed<T>将为 false。 And finally, if T is neither Allowed , nor an object , then it's some primitive we don't want (like symbol ) and so we just return never so that T extends VerifyAllowed<T> will be false.最后,如果T既不是Allowed ,也不是object ,那么这是我们不想要的一些原语(比如symbol ),所以我们只返回never ,这样T extends VerifyAllowed<T>就会是假的。


Okay, so how can we use that?好的,那么我们如何使用它呢? One way if you're using class definitions is to put it in an implements clause to catch any non-compliant class es right way.如果您使用class定义,一种方法是将其放入implements子句中以正确方式捕获任何不符合要求的class This isn't necessary, but without it you'd only catch the error the first time you tried to pass a class instance into something.这不是必需的,但如果没有它,您只会在第一次尝试将 class 实例传递给某物时捕获错误。 Anyway, it looks like this:无论如何,它看起来像这样:

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

Oops, we made a mistake and put y in there.糟糕,我们犯了一个错误,把y放在那里。 Let's remove that:让我们删除它:

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

Whether or not we use implements VerifyAllowed<> in our class declarations, we can still catch mistakes by making any function that accepts "allowed" things generic .无论我们是否在class声明中使用implements VerifyAllowed<> ,我们仍然可以通过使任何接受“允许”事物泛型的 function 来捕捉错误。 For example:例如:

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

}

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

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

Now that we have put the constraint in there we can define Props<T> as the same thing as the Partial<T> utility type , because there's no string index signature messing you up:现在我们已经将约束放在那里,我们可以将Props<T>定义为Partial<T>实用程序类型相同的东西,因为没有字符串索引签名会搞砸你:

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

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

And the same thing goes for AddProps<T> : you can recursively turn T into AddProps<T> without worrying about string index signatures: 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 }
    }
}

Looks good!看起来不错!

Playground link to code Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM