Skip to content

Commit

Permalink
feat(#113): finish @Sealed
Browse files Browse the repository at this point in the history
  • Loading branch information
AshGw committed Apr 26, 2024
1 parent e2e164e commit c9fd003
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 2 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,31 @@ foo.someFoo = () => {
// The line below will cause a TypeError: Cannot assign to read only property 'bar'
foo.bar = 'not bar';
```
You can also [seal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal) an object.
```ts
@Sealed
class Person {
constructor(public name: string, public age?: number) {}
}

const john = new Person('John', 30);

// Existing properties can still be modified
john.age = 31; // No Errors

The TypeScript team has not yet introduced a built-in final modifier, check [this](https://github.com/microsoft/TypeScript/issues/1534), [this](https://github.com/microsoft/TypeScript/issues/8306), [this](https://github.com/microsoft/TypeScript/issues/50532) and many other requests.
// Trying to add a new property will throw an error
(john as any).email = '[email protected]'; // TypeError: Cannot add property email, object is not extensible

// Existing properties cannot be re-configured nor deleted
delete john.age; // TypeError: Cannot delete property 'age'
```
There are many other decorators to choose from, check the [docs](#documentation) for more info.

Speaking of `final`, The TypeScript team has not yet introduced a built-in final modifier yet, check [this](https://github.com/microsoft/TypeScript/issues/1534), [this](https://github.com/microsoft/TypeScript/issues/8306), [this](https://github.com/microsoft/TypeScript/issues/50532) and many other requests.
Although they introduced `override` in [`v4.3`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.html#override-and-the---noimplicitoverride-flag) .

Decorators like ``@Final`` provide a limited way to emulate final behavior, these are merely band-aids for now, until TS officially supports a true final modifier.


#### A type for testing types
```typescript
type ResultType = TestType<Type1, Type2, true>;
Expand Down
36 changes: 36 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1695,3 +1695,39 @@ export function Frozen<T extends Newable>(cst: T): T & Newable {
}
};
}


const _seal = (obj: object) => {
Object.seal(obj);
};
/**
* When applied to a class, it creates a [sealed](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal) instance of it,
* preventing extensions and making existing properties non-configurable.
*
* @example
* ```ts
* @Sealed
* class Person {
* constructor(public name: string, public age?: number) {}
* }
*
* const john = new Person('John', 30);
* // Trying to add a new property will throw an error
* (john as any).email = '[email protected]'; // TypeError: Cannot add property email, object is not extensible
*
* // Existing properties can still be modified
* john.age = 31; // Allowed
*
* // Existing properties cannot be re-configured or deleted
* delete john.age; // TypeError: Cannot delete property 'age'
* }
* ```
* */
export function Sealed<T extends Newable>(cst: T): T & Newable {
return class Locked extends cst {
constructor(...args: any[]) {
super(...args);
_seal(this);
}
};
}
157 changes: 157 additions & 0 deletions tests/sealed-class.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Sealed } from 'src';
import { test, expect } from 'vitest';

test('is the object actually sealed', () => {
@Sealed
class Foo<T> {
private _foo: T;
bar: string;

constructor(foo: T) {
this._foo = foo;
this.bar = 'bar';
}
someFoo(): T {
return this._foo;
}
}
expect(Object.isSealed(new Foo('foo'))).toBeTruthy();
});

test('Should have no problem with instantiation', () => {
@Sealed
class Foo<T> {
private _foo: T;
bar: string;

constructor(foo: T) {
this._foo = foo;
this.bar = 'bar';
}
someFoo(): T {
return this._foo;
}
}

expect(() => {
/* eslint-disable @typescript-eslint/no-unused-vars */
const _ = new Foo('subbedFoo');
}).not.toThrow();
});

test('No problem with instantiation of the sealed class, or the subbed class', () => {
@Sealed
class Foo<T> {
private _foo: T;
bar: string;

constructor(foo: T) {
this._foo = foo;
this.bar = 'bar';
}
someFoo(): T {
return this._foo;
}
}
class SubFoo extends Foo<string> {
constructor(foo: string) {
super(foo);
}
}
expect(() => {
new Foo('foo');
new SubFoo('foo');
}).not.toThrow();
});

test('No problem with instantiation of the sealed already subbed class, or the subbed class from the frozen class', () => {
abstract class BaseFoo<T> {
abstract someFoo(): T;
}
@Sealed
class Foo<T> extends BaseFoo<T> {
private _foo: T;
bar: string;

constructor(foo: T) {
super();
this._foo = foo;
this.bar = 'bar';
}
someFoo(): T {
return this._foo;
}
}
class SubFoo extends Foo<string> {
constructor(foo: string) {
super(foo);
}
}
expect(() => {
new Foo('foo');
new SubFoo('foo');
}).not.toThrow();
});

test('Should be allowed to mutate the properties of a sealed object', () => {
@Sealed
class Foo<T> {
private _foo: T;
bar: string;

constructor(foo: T) {
this._foo = foo;
this.bar = 'bar';
}
someFoo(): T {
return this._foo;
}
}
expect(() => {
const foo = new Foo('foo');
foo.bar = 'altered';
}).not.toThrow(TypeError);
});

test('Should not allow to delete the properties of a sealed object', () => {
@Sealed
class Foo<T> {
private _foo: T;
bar?: string;

constructor(foo: T) {
this._foo = foo;
this.bar = 'bar';
}
someFoo(): T {
return this._foo;
}
}
expect(() => {
delete new Foo('foo').bar;
}).toThrow(TypeError);
});

test('Should work when the final class is a subclass itself', () => {
abstract class BaseFoo<T> {
abstract someFoo(): T;
}
@Sealed
class Foo<T> extends BaseFoo<T> {
private _foo: T;
bar: string;

constructor(foo: T) {
super();
this._foo = foo;
this.bar = 'bar';
}
someFoo(): T {
return this._foo;
}
}
expect(() => {
/* eslint-disable @typescript-eslint/no-unused-vars */
const _ = new Foo('foo').bar;
}).not.toThrow();
});

0 comments on commit c9fd003

Please sign in to comment.