Created
March 8, 2021 15:07
-
-
Save johncrim/b3f1f3b0bdfccaa40d17db73de794371 to your computer and use it in GitHub Desktop.
@Final decorator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export declare interface Type<T> extends Function { | |
new (...args: any[]): T; | |
} | |
function preventSubclassing<T>(constructor: Type<T>): Type<T> { | |
const newCtor = function (...args: any[]): T { | |
if (new.target.prototype !== constructor.prototype) { | |
throw new TypeError(`${constructor.name} cannot be subclassed`); | |
} | |
return new constructor(args); | |
}; | |
// copy prototype so instanceof operator still works | |
newCtor.prototype = constructor.prototype; | |
return newCtor as unknown as Type<T>; | |
} | |
/** | |
* Class decorator that prevents type modification and extension. If the decorated | |
* class is subclassed, a `TypeError` is thrown when the subclass is constructed. | |
* | |
* The name `@final` is used instead of `@sealed` b/c in Javascript, sealing | |
* protects against object modification (not type extension). | |
*/ | |
export function final<T>(constructor: Type<T>): Type<T> { | |
const newCtor = preventSubclassing(constructor); | |
Object.seal(newCtor); | |
Object.freeze(newCtor.prototype); | |
return newCtor; | |
} | |
@final | |
class A { | |
method() { | |
console.log('A.method()'); | |
} | |
} | |
class B extends A { } | |
class C extends A { | |
method() { | |
console.log('C.method()'); | |
} | |
} | |
/** Same as class A, but prototype is modified in test */ | |
@final | |
class AA { | |
method() { | |
console.log('AA.method()'); | |
} | |
} | |
describe('@final', () => { | |
it('decorated type can be constructed', () => { | |
const a = new A(); | |
expect(a).toBeInstanceOf(A); | |
}); | |
it('prevents new()ing a subclass', () => { | |
expect(() => { | |
const b = new B(); | |
}).toThrowMatching(e => e instanceof TypeError); | |
}); | |
it('prevents replacing methods in the decorated prototype', () => { | |
expect(() => { | |
AA.prototype.method = () => { console.log('prototype replaced AA.method()'); }; | |
}).toThrowMatching(e => e instanceof TypeError); | |
}); | |
it('prevents replacing methods in a constructed object', () => { | |
const a = new A(); | |
expect(a).toBeInstanceOf(A); | |
expect(() => { | |
a.method = () => { console.log('instance replaced A.method()'); }; | |
}).toThrowMatching(e => e instanceof TypeError); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment