Add initial project files
This commit is contained in:
Executable
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@crux-kit/types",
|
||||
"version": "1.3.1",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
# Branded Types in TypeScript
|
||||
|
||||
Branded types (also known as _opaque types_) are a TypeScript pattern used to make primitive types like `string` or `number` more specific and safer.
|
||||
|
||||
They allow you to distinguish between values that are structurally identical but conceptually different — for example, an `Email` vs a regular `string`.
|
||||
|
||||
## The Core Idea
|
||||
|
||||
We create a "brand" by intersecting a base type (`string`) with an object containing a unique marker:
|
||||
|
||||
```ts
|
||||
type Branded<T, B extends string> = T & { readonly __brand: B };
|
||||
```
|
||||
|
||||
- `T` → the base type (e.g., `string`)
|
||||
- `B` → a unique label (e.g., `"Email"`)
|
||||
|
||||
This creates a new type that behaves like `T` but is **not interchangeable** with other branded types.
|
||||
|
||||
## Defining a Branded Type
|
||||
|
||||
```ts
|
||||
type Email = Branded<string, "Email">;
|
||||
```
|
||||
|
||||
Now `Email` is no longer just a `string` — it’s a _distinct type_.
|
||||
|
||||
## Creating Values Safely
|
||||
|
||||
To construct a branded value, you typically use a factory function:
|
||||
|
||||
```ts
|
||||
function createEmail(value: string): Email {
|
||||
if (!value.includes("@")) {
|
||||
throw new Error("Invalid email");
|
||||
}
|
||||
return value as Email;
|
||||
}
|
||||
```
|
||||
|
||||
### Why use a function?
|
||||
|
||||
- Centralizes validation
|
||||
- Prevents unsafe casting (`as Email`) scattered across the codebase
|
||||
- Ensures only valid values become branded types
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
const email = createEmail("test@example.com");
|
||||
|
||||
function getEmail(email: Email) {
|
||||
console.log(email);
|
||||
}
|
||||
|
||||
getEmail(email);
|
||||
```
|
||||
|
||||
## Invalid Usage
|
||||
|
||||
```ts
|
||||
const email = createEmail(""); // Throws "Invalid email"
|
||||
```
|
||||
|
||||
```ts
|
||||
getEmail("plain string"); // Type error
|
||||
```
|
||||
|
||||
Even though `"plain string"` is a `string`, it is **not** an `Email`.
|
||||
|
||||
## What Makes It Work?
|
||||
|
||||
This line is the key:
|
||||
|
||||
```ts
|
||||
T & { __brand: B };
|
||||
```
|
||||
|
||||
It creates an **intersection type**, meaning:
|
||||
|
||||
- At runtime -> it's still just a `string`
|
||||
- At compile time -> TypeScript treats it as a unique type
|
||||
|
||||
So this fails:
|
||||
|
||||
```ts
|
||||
type UserId = Branded<string, "UserId">;
|
||||
|
||||
const email: Email = createEmail("a@b.com");
|
||||
const userId: UserId = email; // Type error
|
||||
```
|
||||
|
||||
Even though both are strings, their brands differ.
|
||||
|
||||
## Important Limitation
|
||||
|
||||
Branded types exist **only at compile time**.
|
||||
|
||||
```ts
|
||||
typeof em === "string"; // true
|
||||
```
|
||||
|
||||
There is **no runtime difference** between `Email` and `string`.
|
||||
|
||||
## When to Use Branded Types
|
||||
|
||||
Use them when:
|
||||
|
||||
- You have multiple IDs (`UserId`, `OrderId`, etc.)
|
||||
- You want stronger domain modeling
|
||||
- You want to prevent mixing similar primitives
|
||||
- You want safer APIs without runtime overhead
|
||||
|
||||
## Final Example (Complete)
|
||||
|
||||
```ts
|
||||
type Branded<T, B extends string> = T & { readonly __brand: B };
|
||||
|
||||
type Email = Branded<string, "Email">;
|
||||
|
||||
function createEmail(value: string): Email {
|
||||
if (!value.includes("@")) {
|
||||
throw new Error("Invalid email");
|
||||
}
|
||||
return value as Email;
|
||||
}
|
||||
|
||||
const email = createEmail("damn@example.com");
|
||||
|
||||
function getEmail(email: Email) {
|
||||
console.log(email);
|
||||
}
|
||||
|
||||
getEmail(email);
|
||||
```
|
||||
|
||||
[Readmore](https://dev.to/themuneebh/typescript-branded-types-in-depth-overview-and-use-cases-60e)
|
||||
@@ -0,0 +1 @@
|
||||
export type Branded<T, B extends string> = T & { readonly __brand: B };
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./types";
|
||||
export * from "./branded";
|
||||
@@ -0,0 +1,13 @@
|
||||
export type IsoDateString = string;
|
||||
|
||||
export type TimePeriod = "am" | "pm";
|
||||
|
||||
export type InputTimeFormat = "ISO" | "HMS";
|
||||
|
||||
export type TimeHMS = `${number}:${number}:${number}`;
|
||||
|
||||
export type UserDateInputModeSegment = "day" | "month" | "year";
|
||||
|
||||
export type UserDeviceType = "mobile" | "tablet" | "desktop";
|
||||
|
||||
export type TimeMode = "12h" | "24h";
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2023"],
|
||||
"skipLibCheck": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user