mirror of
https://github.com/sindresorhus/type-fest.git
synced 2025-12-08 19:25:05 +00:00
Make the Opaque type stricter (#71)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
parent
077f8d0784
commit
3ba4cc1ee9
35
source/opaque.d.ts
vendored
35
source/opaque.d.ts
vendored
@ -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};
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user