type-fest/source/opaque.d.ts
Sindre Sorhus 181c203b5e Fix missing export
Fixes #674
2023-08-27 22:42:43 +02:00

206 lines
7.4 KiB
TypeScript

declare const tag: unique symbol;
export type TagContainer<Token> = {
readonly [tag]: Token;
};
type MultiTagContainer<Token extends PropertyKey> = {
readonly [tag]: {[K in Token]: void};
};
/**
Attach a "tag" to an arbitrary type. This allows you to create distinct types, that aren't assignable to one another, for runtime values that would otherwise have the same type. (See examples.)
The generic type parameters can be anything.
Note that `Opaque` is somewhat of a misnomer here, in that, unlike [some alternative implementations](https://github.com/microsoft/TypeScript/issues/4895#issuecomment-425132582), the original, untagged type is not actually hidden. (E.g., functions that accept the untagged type can still be called with the "opaque" version -- but not vice-versa.)
Also note that this implementation is limited to a single tag. If you want to allow multiple tags, use `Tagged` instead.
[Read more about tagged types.](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d)
There have been several discussions about adding similar features to TypeScript. Unfortunately, nothing has (yet) moved forward:
- [Microsoft/TypeScript#202](https://github.com/microsoft/TypeScript/issues/202)
- [Microsoft/TypeScript#15408](https://github.com/Microsoft/TypeScript/issues/15408)
- [Microsoft/TypeScript#15807](https://github.com/Microsoft/TypeScript/issues/15807)
@example
```
import type {Opaque} from 'type-fest';
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;
}
function getMoneyForAccount(accountNumber: AccountNumber): AccountBalance {
return 4 as AccountBalance;
}
// This will compile successfully.
getMoneyForAccount(createAccountNumber());
// But this won't, because it has to be explicitly passed as an `AccountNumber` type.
getMoneyForAccount(2);
// You can use opaque values like they aren't opaque too.
const accountNumber = createAccountNumber();
// This will 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;
};
```
@category Type
*/
export type Opaque<Type, Token = unknown> = Type & TagContainer<Token>;
/**
Revert an opaque or tagged type back to its original type by removing the readonly `[tag]`.
Why is this necessary?
1. Use an `Opaque` type as object keys
2. Prevent TS4058 error: "Return type of exported function has or is using name X from external module Y but cannot be named"
@example
```
import type {Opaque, UnwrapOpaque} from 'type-fest';
type AccountType = Opaque<'SAVINGS' | 'CHECKING', 'AccountType'>;
const moneyByAccountType: Record<UnwrapOpaque<AccountType>, number> = {
SAVINGS: 99,
CHECKING: 0.1
};
// Without UnwrapOpaque, the following expression would throw a type error.
const money = moneyByAccountType.SAVINGS; // TS error: Property 'SAVINGS' does not exist
// Attempting to pass an non-Opaque type to UnwrapOpaque will raise a type error.
type WontWork = UnwrapOpaque<string>;
// Using a Tagged type will work too.
type WillWork = UnwrapOpaque<Tagged<number, 'AccountNumber'>>; // number
```
@category Type
*/
export type UnwrapOpaque<OpaqueType extends TagContainer<unknown>> =
OpaqueType extends MultiTagContainer<string | number | symbol>
? RemoveAllTags<OpaqueType>
: OpaqueType extends Opaque<infer Type, OpaqueType[typeof tag]>
? Type
: OpaqueType;
/**
Attach a "tag" to an arbitrary type. This allows you to create distinct types, that aren't assignable to one another, for runtime values that would otherwise have the same type. (See examples.)
A type returned by `Tagged` can be passed to `Tagged` again, to create a type with multiple tags.
[Read more about tagged types.](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d)
There have been several discussions about adding similar features to TypeScript. Unfortunately, nothing has (yet) moved forward:
- [Microsoft/TypeScript#202](https://github.com/microsoft/TypeScript/issues/202)
- [Microsoft/TypeScript#4895](https://github.com/microsoft/TypeScript/issues/4895)
- [Microsoft/TypeScript#33290](https://github.com/microsoft/TypeScript/pull/33290)
@example
```
import type {Tagged} from 'type-fest';
type AccountNumber = Tagged<number, 'AccountNumber'>;
type AccountBalance = Tagged<number, 'AccountBalance'>;
function createAccountNumber(): AccountNumber {
// As you can see, casting from a `number` (the underlying type being tagged) is allowed.
return 2 as AccountNumber;
}
function getMoneyForAccount(accountNumber: AccountNumber): AccountBalance {
return 4 as AccountBalance;
}
// This will compile successfully.
getMoneyForAccount(createAccountNumber());
// But this won't, because it has to be explicitly passed as an `AccountNumber` type!
getMoneyForAccount(2);
// You can use opaque values like they aren't opaque too.
const accountNumber = createAccountNumber();
// This will compile successfully.
const newAccountNumber = accountNumber + 2;
```
@category Type
*/
export type Tagged<Type, Tag extends PropertyKey> = Type & MultiTagContainer<Tag>;
/**
Revert a tagged type back to its original type by removing the readonly `[tag]`.
Why is this necessary?
1. Use a `Tagged` type as object keys
2. Prevent TS4058 error: "Return type of exported function has or is using name X from external module Y but cannot be named"
@example
```
import type {Tagged, UnwrapTagged} from 'type-fest';
type AccountType = Tagged<'SAVINGS' | 'CHECKING', 'AccountType'>;
const moneyByAccountType: Record<UnwrapTagged<AccountType>, number> = {
SAVINGS: 99,
CHECKING: 0.1
};
// Without UnwrapTagged, the following expression would throw a type error.
const money = moneyByAccountType.SAVINGS; // TS error: Property 'SAVINGS' does not exist
// Attempting to pass an non-Tagged type to UnwrapTagged will raise a type error.
type WontWork = UnwrapTagged<string>;
```
@category Type
*/
export type UnwrapTagged<TaggedType extends MultiTagContainer<PropertyKey>> =
RemoveAllTags<TaggedType>;
type RemoveAllTags<T> = T extends MultiTagContainer<infer ExistingTags>
? {
[ThisTag in ExistingTags]:
T extends Tagged<infer Type, ThisTag>
? RemoveAllTags<Type>
: never
}[ExistingTags]
: T;