Skip to content

Instantly share code, notes, and snippets.

@johncrim
Created March 8, 2021 15:07
Show Gist options
  • Save johncrim/b3f1f3b0bdfccaa40d17db73de794371 to your computer and use it in GitHub Desktop.
Save johncrim/b3f1f3b0bdfccaa40d17db73de794371 to your computer and use it in GitHub Desktop.
@Final decorator
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