Hidden Gems in TypeScript - Short Hands, Aliases, and Underutilized Built-ins That Save You Time

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • MyrinNew
    Senior Member
    • Feb 2024
    • 5168

    #1

    Hidden Gems in TypeScript - Short Hands, Aliases, and Underutilized Built-ins That Save You Time

    TypeScript isn't just about guarding runtime bugs; it's a toolkit for cleaner, safer code with less boilerplate. A handful of built-in aliases, utility types, and subtle syntax tricks can dramatically reduce repetition and the chance of subtle errors. In this post, I'll share practical TypeScript gems you're likely to overlook—short hands, aliases, and underutilized built-ins you can drop into real projects today.


    TL;DR

    • Leverage compact utility types and type aliases (including branded types) to cut boilerplate and prevent mix-ups
    • Use mapped types, conditional types, and as const to create ergonomic, expressive APIs
    • Tap into built-ins like Awaited, Template Literal Types, and discriminated unions for safer, clearer code
    • See concrete patterns: deep partial updates, branded IDs, deep readonly configurations, and discriminated action handling
    • Each example includes a minimal, runnable snippet you can copy-paste into your project


    Prerequisites

    • TypeScript 4.1+ for template literal types; 4.5+ for Awaited; 4.x+ generally fine for most features here
    • Basic familiarity with TypeScript types, interfaces, generics
    • A node/tsconfig project to try snippets locally (optional: a small repo you can clone to test)


    What You'll Learn

    • Practical type tricks: DeepPartial, DeepReadonly, Mutable, branded IDs
    • Short hands and ergonomics: as const, infer in conditional types, ReturnType/Parameters
    • Useful built-ins you may not fully leverage: Awaited, Template Literal Types, discriminated unions, Opaque/branding patterns
    • Real-world patterns: safe config loading, typed IDs, ergonomic API shapes, and robust error handling





    Section 1: Utility Types You Might Be Overlooking

    DeepPartial: Partial Structures at All Nesting Levels

    Need to update nested objects without specifying every field? DeepPartial makes all properties optional recursively:






    type DeepPartial<T> = {
    [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
    };

    // Usage
    type User = {
    id: string;
    profile: {
    name: string;
    bio?: string;
    };
    roles: string[];
    };

    type UpdateUser = DeepPartial<User>;

    // Accepts:
    // { profile: { name?: string }, roles?: string[] }







    NonNullable in Practice

    Extract non-nullable values to ensure type safety:






    type SafeProp<T, K extends keyof T> = T[K] extends null | undefined ? never : T[K];

    // Example constraint
    function getName<T extends { name?: string | null }>(obj: T): string {
    return obj.name ?? "anonymous";
    }










    Section 2: Type Aliases and Branded Types

    Branded/Opaque IDs to Prevent Mixing IDs

    Ever passed the wrong ID to a function? Branded types prevent this at compile time:






    type UserId = string & { __brand?: "UserId" };
    type OrderId = string & { __brand?: "OrderId" };

    function asUserId(id: string): UserId {
    return id as UserId;
    }

    function getUser(userId: UserId) {
    // runtime fetch by id
    }







    Benefit: Runtime value remains a string, but the compiler enforces correct ID usage.


    When to Use Type Aliases vs Interfaces

    Use interfaces for public API shapes and object literals you intend to extend; aliases for unions, primitives, or branded types.





    Section 3: Mapped Types and Conditional Tricks

    Mutable Pattern

    Remove readonly modifiers when you need mutability:






    type Mutable<T> = { -readonly [P in keyof T]: T[P] };

    type ReadonlyPoint = Readonly<{ x: number; y: number }>;
    type Point = Mutable<ReadonlyPoint>;

    const p: Point = { x: 1, y: 2 };
    p.x = 3; // ✅ allowed







    DeepReadonly (Read-Only Everywhere)

    Lock down entire configuration objects:






    type DeepReadonly<T> = {
    readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
    };

    type Config = {
    server: { host: string; port: number };
    features: string[];
    };

    type SafeConfig = DeepReadonly<Config>;







    Distributive Conditional Tricks





    type IsStringLike<T> = T extends string ? true : false;
    type A = IsStringLike<string | number>; // true | false










    Section 4: Short Hands and Ergonomic Syntax

    as const for Literal Narrowing and Discriminated Unions





    const FETCH_USERS = { type: "FETCH_USERS" } as const;
    type Action = typeof FETCH_USERS;

    type Response =
    | { status: "ok"; data: string[] }
    | { status: "error"; error: string };

    function handle(res: Response) {
    if (res.status === "ok") {
    // res.data is string[]
    }
    }







    infer in Conditional Types

    Extract types from arrays or other generic structures:






    type ElementType<T> = T extends (infer U)[] ? U : T;

    type T1 = ElementType<string[]>; // string
    type T2 = ElementType<number>; // number







    Awaited for Unwrapping Promises





    async function fetchData(): Promise<{ ok: boolean }> {
    return { ok: true };
    }
    type Data = Awaited<ReturnType<typeof fetchData>>; // { ok: boolean }







    Template Literal Types for Chemistry Between Strings and Types





    type RoutePath = `/users/${string}` | `/projects/${string}`;

    function navigate(path: RoutePath) {
    // runtime navigation
    }










    Section 5: Built-ins You May Not Fully Leverage

    • Opaque branding via intersection (as shown with UserId/OrderId)
    • Template literal types to encode string shapes in types
    • Yielding with Generator types (advanced exercise)
    • AsyncReturnType pattern (a predecessor to Awaited in 4.x)


    Note: Some of these require careful design to avoid over-engineering—use where they solve a real pain point.





    Section 6: Practical Patterns with Concrete, Minimal Examples

    Example 1: Safe Configuration Loader





    type Config = {
    port?: number;
    host?: string;
    };

    type RequiredConfig = Required<Config>;

    function loadConfig(input: Partial<Config>): Config {
    const defaultConfig: Config = { port: 3000, host: "localhost" };
    return { ...defaultConfig, ...input };
    }

    // Usage
    const cfg = loadConfig({ host: "example.com" });







    Example 2: Discriminated Union for API Responses





    type ApiResponse =
    | { ok: true; data: string[] }
    | { ok: false; error: string };

    function handleResponse(r: ApiResponse) {
    if (r.ok) {
    // r.data is string[]
    console.log("Got data", r.data);
    } else {
    console.error("API error", r.error);
    }
    }







    Example 3: Function Ergonomics with Generics and ReturnType





    function wrap<T>(value: T) {
    return { value };
    }

    type Wrapped<T> = ReturnType<typeof wrap<T>>;










    Section 7: Pitfalls and Tips

    • Complexity vs readability: Advanced types are powerful but can obscure intent
    • Prefer clear intent before cleverness; add comments documenting the rationale for branding or DeepPartial types
    • When in doubt, validate with a small runtime test to ensure the type-level trick aligns with runtime semantics
    • Keep snippets minimal and focused; readers often skim dense type theory





    Section 8: Real-World Integration Tips

    • Introduce in-code comments near branded types to explain safety guarantees
    • If you're adding a branded ID pattern, audit runtime code to ensure IDs don't slip through unbranded
    • Use TypeScript's type-level utilities incrementally; start with DeepPartial or DeepReadonly for configuration-heavy modules





    Conclusion

    TypeScript's power isn't only in what it can prevent at runtime, but in how it shapes safer, more expressive APIs with less boilerplate. The hidden gems above—short hands, aliases, and underutilized built-ins—are practical levers you can pull today to improve readability, safety, and developer experience in your codebase.





    References and Credits

    • TypeScript Handbook (type utilities, mapped types, conditional types)
    • TypeScript 4.x features: Awaited, template literal types, infer
    • Community patterns: branded types, DeepPartial, DeepReadonly patterns





    What TypeScript hidden gems do you use in your projects? Share your favorites in the comments below!




    More...
Working...