Make the Opaque type stricter (#71)

Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
Resynth 2020-01-31 17:33:16 +00:00 committed by GitHub
parent 077f8d0784
commit 3ba4cc1ee9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 37 additions and 9 deletions

35
source/opaque.d.ts vendored
View File

@ -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;
}
@ -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};

View File

@ -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