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 2 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
29 changes: 24 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 ThingOnw = Opaque<string>;
resynth1943 marked this conversation as resolved.
Show resolved Hide resolved
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 NewThingOnw = Opaque<string, 'ThingOne'>;
resynth1943 marked this conversation as resolved.
Show resolved Hide resolved
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,8 @@ 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;
```
*/
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);