TypeScript Type Manipulation — Complete Guide
In this tutorial, you will learn about TypeScript Type Manipulation. We cover key concepts, practical examples, and best practices to help you master this topic.
TypeScript type manipulation techniques like satisfies and branded types give you finer control over Type Checking — satisfies validates types without widening, while branded types simulate nominal typing for scenarios where structural typing is too permissive.
What You'll Learn
- The
satisfiesoperator (TS 4.9+) satisfiesvsas— when to use each- Branded types for nominal typing
- Flavoring types for distinct identity
- Practical patterns for type-safe IDs, currencies, and units
Why It Matters
TypeScript's structural type system is powerful but sometimes too loose — it considers string equal to string even when one is a user ID and the other is an email. Branded types add compile-time identity to structurally identical types. satisfies catches mistakes without changing the inferred literal type.
Real-World Use
Durga Antivirus Pro uses branded types for UserId, ScanId, and ThreatId — you cannot accidentally pass a scan ID where a user ID is expected, even though both are string. The Doda Browser extension configuration uses satisfies to validate that a config object matches a schema while preserving its literal types for autocompletion.
Learning Path
flowchart LR A[Declaration Files] --> B[Type Manipulation] B --> C[Classes & OOP] B --> D[You Are Here] C --> E[Inheritance] E --> F[Decorators]
The satisfies Operator
Introduced in TypeScript 4.9, satisfies checks that a value's type matches a given type without widening the value's inferred type:
type Color = "red" | "green" | "blue";
const palette1: Record<string, Color> = {
primary: "red",
secondary: "blue",
// accent: "purple", // Error: not in Color
};
// Problem: palette1.primary is inferred as Color (wider than "red")
const palette2 = {
primary: "red",
secondary: "blue",
accent: "purple", // No error — no type annotation!
} satisfies Record<string, Color>; // Error: "purple" is not in Color
// palette2.primary is inferred as "red" (literal type preserved!)
Think of satisfies as: "Check that this value matches the type, but keep the precise literal type for autocompletion."
satisfies vs as
type Point = { x: number; y: number };
const p1 = { x: 10, y: 20 } as Point;
// p1.x is type: number (widened)
const p2 = { x: 10, y: 20 } satisfies Point;
// p2.x is type: 10 (literal preserved!)
// as: assumes you're right (type assertion)
// satisfies: checks you're right (type validation)
satisfies with Arrays
const colors = ["red", "green", "blue"] satisfies string[];
// colors is inferred as string[], fine
const specific = ["red", "green", "blue"] satisfies [string, string, string];
// specific is inferred as [string, string, string]
satisfies with Complex Objects
interface RouteConfig {
path: string;
component: string;
children?: RouteConfig[];
meta?: Record<string, unknown>;
}
const routes = [
{
path: "/",
component: "Home",
},
{
path: "/users",
component: "UserList",
meta: {
requiresAuth: true,
},
},
] satisfies RouteConfig[];
// routes[0].path is "string" but inferred as "/" literal
// routes[0].meta?.requiresAuth // Error: meta may not exist on / route — correctly caught!
Branded Types
Branded types add a compile-only property to distinguish structurally identical types:
type UserId = string & { __brand: "UserId" };
type ScanId = string & { __brand: "ScanId" };
type Email = string & { __brand: "Email" };
function createUserId(id: string): UserId {
return id as UserId;
}
function createScanId(id: string): ScanId {
return id as ScanId;
}
function getUser(id: UserId): void {
console.log(`Getting user ${id}`);
}
function getScan(id: ScanId): void {
console.log(`Getting scan ${id}`);
}
const userId = createUserId("usr-001");
const scanId = createScanId("scan-001");
getUser(userId); // OK
// getUser(scanId); // Error: ScanId is not assignable to UserId
// getUser("usr-002"); // Error: string is not assignable to UserId
Think of brands as tamper-proof labels — two strings look the same, but their brands distinguish them at compile time. The brand property doesn't exist at runtime; it's erased during compilation.
Brand Helper
declare const brand: unique symbol;
type Brand<T, B extends string> = T & { [brand]: B };
type UserId = Brand<string, "UserId">;
type ScanId = Brand<string, "ScanId">;
function create<T, B extends string>(value: T, _brand: B): Brand<T, B> {
return value as Brand<T, B>;
}
const uid = create("usr-001", "UserId");
const sid = create("scan-001", "ScanId");
Number Brands
type Seconds = Brand<number, "Seconds">;
type Milliseconds = Brand<number, "Milliseconds">;
function toMs(seconds: Seconds): Milliseconds {
return (seconds * 1000) as Milliseconds;
}
const timeout = 30 as Seconds;
const delay = toMs(timeout); // type: Milliseconds
// toMs(delay); // Error: Milliseconds is not Seconds
Flavoring (a Lighter Brand)
Flavoring doesn't require a brand property — it uses a phantom type parameter:
type Flavor<T, F> = T & { __flavor?: F };
type USD = Flavor<number, "USD">;
type EUR = Flavor<number, "EUR">;
function createUSD(amount: number): USD {
return amount as USD;
}
const price = createUSD(29.99);
// Number operations still work:
const total = (price * 1.1) as USD; // Cast needed after operations
Practical Branding Patterns
Type-Safe API Parameters
type ArticleId = Brand<string, "ArticleId">;
type CommentId = Brand<string, "CommentId">;
declare function getArticle(id: ArticleId): Promise<Article>;
declare function getComment(id: CommentId): Promise<Comment>;
// Cannot accidentally swap them
// getComment(articleId); // Error
Currency Safety
type USD = Brand<number, "USD">;
type EUR = Brand<number, "EUR">;
function usd(amount: number): USD { return amount as USD; }
function eur(amount: number): EUR { return amount as EUR; }
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const price = usd(29.99);
const tax = usd(3.00);
const total = addUSD(price, tax); // OK: USD + USD = USD
// addUSD(price, eur(10)); // Error: EUR is not USD
Unit Safety
type Meters = Brand<number, "Meters">;
type Feet = Brand<number, "Feet">;
function meters(value: number): Meters { return value as Meters; }
function feet(value: number): Feet { return value as Feet; }
const roomWidth = meters(10);
// roomWidth + 5 // OK: number operations work
// addMeters(roomWidth, feet(3)) // Error
Common Mistakes
1. Using as Instead of satisfies When You Want Validation
type Config = { debug: boolean; timeout: number };
// Bad: as disables validation for excess properties
const config = { debug: true, timeout: 5000, extra: "oops" } as Config;
// Good: satisfies catches the error
const config2 = { debug: true, timeout: 5000 } satisfies Config;
2. Forgetting That Brands Are Compile-Time Only
type UserId = Brand<string, "UserId">;
const uid: UserId = "abc" as UserId;
// At runtime, uid is just "abc" — any library receiving it sees a plain string
// Brand is not validated at runtime — it's purely for compile-time safety
3. Accidentally Creating a Non-Overlapping Brand
// This brand type is impossible — string & number cannot overlap
type Impossible = string & number & { __brand: "test" };
// Result: never
4. Using satisfies When You Need a Type Assertion
// satisfies checks the type but doesn't narrow
const data = JSON.parse(raw) satisfies User;
// data is still unknown! satisfies didn't narrow it.
// Use "as" when you need to narrow:
const data = JSON.parse(raw) as User;
5. Brand Pollution Through Operations
type USD = Brand<number, "USD">;
const money = 100 as USD;
const doubled = money * 2; // Type: number (brand lost!)
// Fix: const doubled = (money * 2) as USD;
6. Using Brands Unnecessarily
Not every value needs a brand. Use brands only when two structurally identical types must be distinguished for correctness (currency, units, IDs).
Practice Questions
What does the
satisfiesoperator do thatasdoes not?satisfiesvalidates the type without widening it, preserving literal types.asasserts a type without validation.Why do we need branded types in TypeScript? TypeScript has structural typing — two types with the same shape are considered the same. Brands add compile-time distinction to structurally identical types.
How does a branded type work at runtime? The brand property is erased during compilation. At runtime, the value is just its underlying type (e.g., a plain string).
What is the difference between branding and flavoring? Branding uses a required intersection with a unique symbol. Flavoring uses an optional phantom type parameter, making it slightly less strict.
Challenge: Create a branded type system for a recipe app with Teaspoon, Tablespoon, and Cup types. Write functions to convert between them with compile-time safety.
FAQ
Mini Project: Type-Safe Measurement System
// src/measurements.ts
declare const brand: unique symbol;
type Brand<T, B extends string> = T & { [brand]: B };
type Meters = Brand<number, "Meters">;
type Kilometers = Brand<number, "Kilometers">;
type Miles = Brand<number, "Miles">;
function meters(value: number): Meters { return value as Meters; }
function kilometers(value: number): Kilometers { return value as Kilometers; }
function miles(value: number): Miles { return value as Miles; }
function kmToMiles(km: Kilometers): Miles {
return (km * 0.621371) as Miles;
}
function kmToMeters(km: Kilometers): Meters {
return (km * 1000) as Meters;
}
function addMeters(a: Meters, b: Meters): Meters {
return (a + b) as Meters;
}
const distance = kilometers(5);
const inMiles = kmToMiles(distance);
const inMeters = kmToMeters(distance);
console.log(`5 km = ${inMiles} miles`); // 5 km = 3.106855 miles
console.log(`5 km = ${inMeters} meters`); // 5 km = 5000 meters
const leg1 = meters(1000);
const leg2 = meters(2500);
const total = addMeters(leg1, leg2);
console.log(`Total walk: ${total} meters`); // 3500 meters
// Type errors (uncomment to test):
// kmToMiles(meters(100)); // Error: Meters is not Kilometers
// addMeters(leg1, distance); // Error: Kilometers is not Meters
What's Next
You've completed Module 2: Advanced Types! Now dive into Classes and OOP:
| Lesson | Description |
|---|---|
| {{< ref "/programming-languages/typescript/17-declaration-files" >}} | Review declaration files |
| {{< ref "/programming-languages/typescript/19-classes" >}} | Class syntax, implements, access modifiers |
| {{< ref "/programming-languages/typescript/20-abstract-classes" >}} | Abstract classes and static members |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro