Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the Opaque type stricter #71

Merged
merged 6 commits into from
Jan 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 30 additions & 5 deletions source/opaque.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,28 @@ There have been several discussions about adding this feature to TypeScript via
```
import {Opaque} from 'type-fest';

type AccountNumber = Opaque<number>;
type AccountBalance = Opaque<number>;
type AccountNumber = Opaque<number, 'AccountNumber'>;
type AccountBalance = Opaque<number, 'AccountBalance'>;

// The Token parameter allows the compiler to differentiate between types, whereas "unknown" will not. For example, consider the following structures:
type ThingOne = Opaque<string>;
type ThingTwo = Opaque<string>;

// To the compiler, these types are allowed to be cast to each other as they have the same underlying type. They are both `string & { __opaque__: unknown }`.
// To avoid this behaviour, you would instead pass the "Token" parameter, like so.
type NewThingOne = Opaque<string, 'ThingOne'>;
type NewThingTwo = Opaque<string, 'ThingTwo'>;

// Now they're completely separate types, so the following will fail to compile.
function createNewThingOne (): NewThingOne {
// As you can see, casting from a string is still allowed. However, you may not cast NewThingOne to NewThingTwo, and vice versa.
return 'new thing one' as NewThingOne;
}

// This will fail to compile, as they are fundamentally different types.
const thingTwo = createNewThingOne() as NewThingTwo;

// Here's another example of opaque typing.
function createAccountNumber(): AccountNumber {
return 2 as AccountNumber;
}
Expand All @@ -33,8 +52,14 @@ getMoneyForAccount(2);
// You can use opaque values like they aren't opaque too.
const accountNumber = createAccountNumber();

// This will compile successfully.
accountNumber + 2;
// This will not compile successfully.
const newAccountNumber = accountNumber + 2;

// As a side note, you can (and should) use recursive types for your opaque types to make them stronger and hopefully easier to type.
type Person = {
id: Opaque<number, Person>;
name: string;
};
```
*/
export type Opaque<Type> = Type & {readonly __opaque__: unique symbol};
export type Opaque<Type, Token = unknown> = Type & {readonly __opaque__: Token};
11 changes: 7 additions & 4 deletions test-d/opaque.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import {expectType} from 'tsd';
import {expectType, expectError} from 'tsd';
import {Opaque} from '..';

type Value = Opaque<number>;
type Value = Opaque<number, 'Value'>;

// We make an explicit cast so we can test the value.
const value: Value = 2 as Value;

// Every opaque type should have a private symbol member, so the compiler can differentiate separate opaque types.
expectType<symbol>(value.__opaque__);
// Every opaque type should have a private member with a type of the Token parameter, so the compiler can differentiate separate opaque types.
expectType<unknown>(value.__opaque__);

// The underlying type of the value is still a number.
expectType<number>(value);

// You cannot modify an opaque value.
expectError<Value>(value + 2); // eslint-disable-line @typescript-eslint/restrict-plus-operands