Mastering TypeScript - Advanced Techniques for Better Code

TypeScript has become the de facto standard for large-scale JavaScript applications. While many developers are comfortable with basic typing, mastering advanced TypeScript features can dramatically improve code quality and developer experience.

Advanced Type Utilities

Conditional Types

Create types that change based on conditions:

type ApiResponse<T> = T extends string ? { message: T } : T extends number ? { code: T } : { data: T };

// Usage
type StringResponse = ApiResponse<string>; // { message: string }
type NumberResponse = ApiResponse<number>; // { code: number }
type ObjectResponse = ApiResponse<User>; // { data: User }

Mapped Types

Transform existing types:

type Optional<T> = {
    [K in keyof T]?: T[K];
};

type ReadonlyPartial<T> = {
    readonly [K in keyof T]?: T[K];
};

interface User {
    id: number;
    name: string;
    email: string;
}

type PartialUser = Optional<User>; // All properties optional
type ReadonlyUser = ReadonlyPartial<User>; // All properties optional and readonly

Template Literal Types

Create sophisticated string manipulation at the type level:

type EventName<T extends string> = `on${Capitalize<T>}`;
type HttpMethod = "get" | "post" | "put" | "delete";
type ApiEndpoint<T extends HttpMethod> = `/${T}Api`;

// Usage
type ClickHandler = EventName<"click">; // "onClick"
type PostEndpoint = ApiEndpoint<"post">; // "/postApi"

// Advanced template literals
type CSSProperty<T extends string> = `--${T}`;
type CSSVariable = CSSProperty<"primary-color" | "font-size">; // "--primary-color" | "--font-size"

Discriminated Unions

Create type-safe state management:

type LoadingState = {
    status: "loading";
    data: null;
    error: null;
};

type SuccessState<T> = {
    status: "success";
    data: T;
    error: null;
};

type ErrorState = {
    status: "error";
    data: null;
    error: string;
};

type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;

function handleState<T>(state: AsyncState<T>) {
    switch (state.status) {
        case "loading":
            // TypeScript knows data is null here
            return "Loading...";
        case "success":
            // TypeScript knows data is T here
            return `Success: ${state.data}`;
        case "error":
            // TypeScript knows error is string here
            return `Error: ${state.error}`;
    }
}

Advanced Generics

Constraint-based Generics

interface Identifiable {
    id: string | number;
}

function updateEntity<T extends Identifiable>(entities: T[], id: T["id"], updates: Partial<Omit<T, "id">>): T[] {
    return entities.map((entity) => (entity.id === id ? { ...entity, ...updates } : entity));
}

// Usage
const users = [
    { id: 1, name: "John", email: "john@example.com" },
    { id: 2, name: "Jane", email: "jane@example.com" },
];

const updatedUsers = updateEntity(users, 1, { name: "John Doe" });

Variadic Tuple Types

type Head<T extends readonly unknown[]> = T extends readonly [infer H, ...unknown[]] ? H : never;
type Tail<T extends readonly unknown[]> = T extends readonly [unknown, ...infer T] ? T : [];

type Last<T extends readonly unknown[]> = T extends readonly [...unknown[], infer L] ? L : never;

// Function composition with type safety
type Pipe<T extends readonly [(...args: any[]) => any, ...Array<(arg: any) => any>]> = T extends readonly [
    (...args: any[]) => infer U,
    ...infer R,
]
    ? R extends readonly [(arg: U) => any, ...Array<(arg: any) => any>]
        ? Pipe<R> extends (...args: any[]) => infer V
            ? (...args: Parameters<T[0]>) => V
            : never
        : T[0]
    : never;

declare function pipe<T extends readonly [(...args: any[]) => any, ...Array<(arg: any) => any>]>(...fns: T): Pipe<T>;

// Usage
const add = (a: number, b: number) => a + b;
const double = (x: number) => x * 2;
const toString = (x: number) => x.toString();

const composed = pipe(add, double, toString);
const result = composed(5, 3); // "16" (type-safe!)

Module Augmentation

Extend existing types safely:

// Extending Express Request type
declare global {
    namespace Express {
        interface Request {
            user?: {
                id: string;
                role: "admin" | "user";
            };
        }
    }
}

// Extending built-in Array type
declare global {
    interface Array<T> {
        groupBy<K extends PropertyKey>(keySelector: (item: T) => K): Record<K, T[]>;
    }
}

Array.prototype.groupBy = function <T>(this: T[], keySelector: (item: T) => PropertyKey) {
    return this.reduce(
        (groups, item) => {
            const key = keySelector(item);
            if (!groups[key]) {
                groups[key] = [];
            }
            groups[key].push(item);
            return groups;
        },
        {} as Record<PropertyKey, T[]>
    );
};

Brand Types for Enhanced Type Safety

Create nominal types in a structural type system:

type Brand<T, K> = T & { readonly __brand: K };

type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;

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

function createPostId(id: string): PostId {
    return id as PostId;
}

function getUser(id: UserId): User {
    // Implementation
}

function getPost(id: PostId): Post {
    // Implementation
}

// Usage
const userId = createUserId("user-123");
const postId = createPostId("post-456");

getUser(userId); // ✅ Correct
getPost(postId); // ✅ Correct
getUser(postId); // ❌ TypeScript error - can't pass PostId where UserId expected

Performance Optimization Tips

1. Use const assertions for better inference

// Instead of this
const colors = ["red", "green", "blue"]; // string[]

// Use this
const colors = ["red", "green", "blue"] as const; // readonly ["red", "green", "blue"]

2. Avoid excessive type instantiation

// Problematic: Creates new type for each instantiation
type BadApiCall<T> = {
    endpoint: string;
    data: T;
    metadata: {
        timestamp: number;
        userId: string;
        requestId: string;
    };
};

// Better: Reuse common parts
type RequestMetadata = {
    timestamp: number;
    userId: string;
    requestId: string;
};

type ApiCall<T> = {
    endpoint: string;
    data: T;
    metadata: RequestMetadata;
};

Conclusion

Advanced TypeScript features enable you to build more robust, maintainable applications with better developer experience. Start incorporating these patterns gradually into your projects to see immediate benefits in code quality and type safety.

Remember: The goal isn't to use every advanced feature, but to choose the right tools for your specific use cases. TypeScript's power lies in its ability to scale from simple type annotations to sophisticated type-level programming.