I need to restrict the properties names and the types a class can have. 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. 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. Even a simple type which iterates over the properties of B, will not behave as desired because keyof T includes [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
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
Question
Is there a way to achieve what I am after? Playground link