Source: wshobson/agents Original Plugin: javascript-typescript
TypeScript Advanced Types
Comprehensive guidance for mastering TypeScript's advanced type system including generics, conditional types, mapped types, template literal types, and utility types for building robust, type-safe applications.
When to Use This Skill
- Building type-safe libraries or frameworks
- Creating reusable generic components
- Implementing complex type inference logic
- Designing type-safe API clients
- Building form validation systems
- Creating strongly-typed configuration objects
- Implementing type-safe state management
- Migrating JavaScript codebases to TypeScript
Core Concepts
1. Generics
Purpose: Create reusable, type-flexible components while maintaining type safety.
Basic Generic Function:
function identity<T>(value: T): T {
  return value;
}
const num = identity<number>(42);        // Type: number
const str = identity<string>("hello");    // Type: string
const auto = identity(true);              // Type inferred: boolean
Generic Constraints:
interface HasLength {
  length: number;
}
function logLength<T extends HasLength>(item: T): T {
  console.log(item.length);
  return item;
}
logLength("hello");           // OK: string has length
logLength([1, 2, 3]);         // OK: array has length
logLength({ length: 10 });    // OK: object has length
// logLength(42);             // Error: number has no length
Multiple Type Parameters:
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}
const merged = merge(
  { name: "John" },
  { age: 30 }
);
// Type: { name: string } & { age: number }
2. Conditional Types
Purpose: Create types that depend on conditions, enabling sophisticated type logic.
Basic Conditional Type:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;    // true
type B = IsString<number>;    // false
Extracting Return Types:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
  return { id: 1, name: "John" };
}
type User = ReturnType<typeof getUser>;
// Type: { id: number; name: string; }
Distributive Conditional Types:
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>;
// Type: string[] | number[]
Nested Conditions:
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";
type T1 = TypeName<string>;     // "string"
type T2 = TypeName<() => void>; // "function"
3. Mapped Types
Purpose: Transform existing types by iterating over their properties.
Basic Mapped Type:
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};
interface User {
  id: number;
  name: string;
}
type ReadonlyUser = Readonly<User>;
// Type: { readonly id: number; readonly name: string; }
Optional Properties:
type Partial<T> = {
  [P in keyof T]?: T[P];
};
type PartialUser = Partial<User>;
// Type: { id?: number; name?: string; }
Key Remapping:
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Person {
  name: string;
  age: number;
}
type PersonGetters = Getters<Person>;
// Type: { getName: () => string; getAge: () => number; }
Filtering Properties:
type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K]
};
interface Mixed {
  id: number;
  name: string;
  age: number;
  active: boolean;
}
type OnlyNumbers = PickByType<Mixed, number>;
// Type: { id: number; age: number; }
4. Template Literal Types
Purpose: Create string-based types with pattern matching and transformation.
Basic Template Literal:
type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// Type: "onClick" | "onFocus" | "onBlur"
String Manipulation:
type UppercaseGreeting = Uppercase<"hello">;  // "HELLO"
type LowercaseGreeting = Lowercase<"HELLO">;  // "hello"
type CapitalizedName = Capitalize<"john">;    // "John"
type UncapitalizedName = Uncapitalize<"John">; // "john"
Path Building:
type Path<T> = T extends object
  ? { [K in keyof T]: K extends string
      ? `${K}` | `${K}.${Path<T[K]>}`
      : never
    }[keyof T]
  : never;
interface Config {
  server: {
    host: string;
    port: number;
  };
  database: {
    url: string;
  };
}
type ConfigPath = Path<Config>;
// Type: "server" | "database" | "server.host" | "server.port" | "database.url"
5. Utility Types
Built-in Utility Types:
// Partial<T> - Make all properties optional
type PartialUser = Partial<User>;
// Required<T> - Make all properties required
type RequiredUser = Required<PartialUser>;
// Readonly<T> - Make all properties readonly
type ReadonlyUser = Readonly<User>;
// Pick<T, K> - Select specific properties
type UserName = Pick<User, "name" | "email">;
// Omit<T, K> - Remove specific properties
type UserWithoutPassword = Omit<User, "password">;
// Exclude<T, U> - Exclude types from union
type T1 = Exclude<"a" | "b" | "c", "a">;  // "b" | "c"
// Extract<T, U> - Extract types from union
type T2 = Extract<"a" | "b" | "c", "a" | "b">;  // "a" | "b"
// NonNullable<T> - Exclude null and undefined
type T3 = NonNullable<string | null | undefined>;  // string
// Record<K, T> - Create object type with keys K and values T
type PageInfo = Record<"home" | "about", { title: string }>;
Advanced Patterns
Pattern 1: Type-Safe Event Emitter
type EventMap = {
  "user:created": { id: string; name: string };
  "user:updated": { id: string };
  "user:deleted": { id: string };
};
class TypedEventEmitter<T extends Record<string, any>> {
  private listeners: {
    [K in keyof T]?: Array<(data: T[K]) => void>;
  } = {};
  on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(callback);
  }
  emit<K extends keyof T>(event: K, data: T[K]): void {
    const callbacks = this.listeners[event];
    if (callbacks) {
      callbacks.forEach(callback => callback(data));
    }
  }
}
const emitter = new TypedEventEmitter<EventMap>();
emitter.on("user:created", (data) => {
  console.log(data.id, data.name);  // Type-safe!
});
emitter.emit("user:created", { id: "1", name: "John" });
// emitter.emit("user:created", { id: "1" });  // Error: missing 'name'
Pattern 2: Type-Safe API Client
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type EndpointConfig = {
  "/users": {
    GET: { response: User[] };
    POST: { body: { name: string; email: string }; response: User };
  };
  "/users/:id": {
    GET: { params: { id: string }; response: User };
    PUT: { params: { id: string }; body: Partial<User>; response: User };
    DELETE: { params: { id: string }; response: void };
  };
};
type ExtractParams<T> = T extends { params: infer P } ? P : never;
type ExtractBody<T> = T extends { body: infer B } ? B : never;
type ExtractResponse<T> = T extends { response: infer R } ? R : never;
class APIClient<Config extends Record<string, Record<HTTPMethod, any>>> {
  async request<
    Path extends keyof Config,
    Method extends keyof Config[Path]
  >(
    path: Path,
    method: Method,
    ...[options]: ExtractParams<Config[Path][Method]> extends never
      ? ExtractBody<Config[Path][Method]> extends never
        ? []
        : [{ body: ExtractBody<Config[Path][Method]> }]
      : [{
          params: ExtractParams<Config[Path][Method]>;
          body?: ExtractBody<Config[Path][Method]>;
        }]
  ): Promise<ExtractResponse<Config[Path][Method]>> {
    // Implementation here
    return {} as any;
  }
}
const api = new APIClient<EndpointConfig>();
// Type-safe API calls
const users = await api.request("/users", "GET");
// Type: User[]
const newUser = await api.request("/users", "POST", {
  body: { name: "John", email: "john@example.com" }
});
// Type: User
const user = await api.request("/users/:id", "GET", {
  params: { id: "123" }
});
// Type: User
Pattern 3: Builder Pattern with Type Safety
type BuilderState<T> = {
  [K in keyof T]: T[K] | undefined;
};
type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];
type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
type IsComplete<T, S> =
  RequiredKeys<T> extends keyof S
    ? S[RequiredKeys<T>] extends undefined
      ? false
      : true
    : false;
class Builder<T, S extends BuilderState<T> = {}> {
  private state: S = {} as S;
  set<K extends keyof T>(
    key: K,
    value: T[K]
  ): Builder<T, S & Record<K, T[K]>> {
    this.state[key] = value;
    return this as any;
  }
  build(
    this: IsComplete<T, S> extends true ? this : never
  ): T {
    return this.state as T;
  }
}
interface User {
  id: string;
  name: string;
  email: string;
  age?: number;
}
const builder = new Builder<User>();
const user = builder
  .set("id", "1")
  .set("name", "John")
  .set("email", "john@example.com")
  .build();  // OK: all required fields set
// const incomplete = builder
//   .set("id", "1")
//   .build();  // Error: missing required fields
Pattern 4: Deep Readonly/Partial
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepReadonly<T[P]>
    : T[P];
};
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepPartial<U>>
      : DeepPartial<T[P]>
    : T[P];
};
interface Config {
  server: {
    host: string;
    port: number;
    ssl: {
      enabled: boolean;
      cert: string;
    };
  };
  database: {
    url: string;
    pool: {
      min: number;
      max: number;
    };
  };
}
type ReadonlyConfig = DeepReadonly<Config>;
// All nested properties are readonly
type PartialConfig = DeepPartial<Config>;
// All nested properties are optional
Pattern 5: Type-Safe Form Validation
type ValidationRule<T> = {
  validate: (value: T) => boolean;
  message: string;
};
type FieldValidation<T> = {
  [K in keyof T]?: ValidationRule<T[K]>[];
};
type ValidationErrors<T> = {
  [K in keyof T]?: string[];
};
class FormValidator<T extends Record<string, any>> {
  constructor(private rules: FieldValidation<T>) {}
  validate(data: T): ValidationErrors<T> | null {
    const errors: ValidationErrors<T> = {};
    let hasErrors = false;
    for (const key in this.rules) {
      const fieldRules = this.rules[key];
      const value = data[key];
      if (fieldRules) {
        const fieldErrors: string[] = [];
        for (const rule of fieldRules) {
          if (!rule.validate(value)) {
            fieldErrors.push(rule.message);
          }
        }
        if (fieldErrors.length > 0) {
          errors[key] = fieldErrors;
          hasErrors = true;
        }
      }
    }
    return hasErrors ? errors : null;
  }
}
interface LoginForm {
  email: string;
  password: string;
}
const validator = new FormValidator<LoginForm>({
  email: [
    {
      validate: (v) => v.includes("@"),
      message: "Email must contain @"
    },
    {
      validate: (v) => v.length > 0,
      message: "Email is required"
    }
  ],
  password: [
    {
      validate: (v) => v.length >= 8,
      message: "Password must be at least 8 characters"
    }
  ]
});
const errors = validator.validate({
  email: "invalid",
  password: "short"
});
// Type: { email?: string[]; password?: string[]; } | null
Pattern 6: Discriminated Unions
type Success<T> = {
  status: "success";
  data: T;
};
type Error = {
  status: "error";
  error: string;
};
type Loading = {
  status: "loading";
};
type AsyncState<T> = Success<T> | Error | Loading;
function handleState<T>(state: AsyncState<T>): void {
  switch (state.status) {
    case "success":
      console.log(state.data);  // Type: T
      break;
    case "error":
      console.log(state.error);  // Type: string
      break;
    case "loading":
      console.log("Loading...");
      break;
  }
}
// Type-safe state machine
type State =
  | { type: "idle" }
  | { type: "fetching"; requestId: string }
  | { type: "success"; data: any }
  | { type: "error"; error: Error };
type Event =
  | { type: "FETCH"; requestId: string }
  | { type: "SUCCESS"; data: any }
  | { type: "ERROR"; error: Error }
  | { type: "RESET" };
function reducer(state: State, event: Event): State {
  switch (state.type) {
    case "idle":
      return event.type === "FETCH"
        ? { type: "fetching", requestId: event.requestId }
        : state;
    case "fetching":
      if (event.type === "SUCCESS") {
        return { type: "success", data: event.data };
      }
      if (event.type === "ERROR") {
        return { type: "error", error: event.error };
      }
      return state;
    case "success":
    case "error":
      return event.type === "RESET" ? { type: "idle" } : state;
  }
}
Type Inference Techniques
1. Infer Keyword
// Extract array element type
type ElementType<T> = T extends (infer U)[] ? U : never;
type NumArray = number[];
type Num = ElementType<NumArray>;  // number
// Extract promise type
type PromiseType<T> = T extends Promise<infer U> ? U : never;
type AsyncNum = PromiseType<Promise<number>>;  // number
// Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function foo(a: string, b: number) {}
type FooParams = Parameters<typeof foo>;  // [string, number]
2. Type Guards
function isString(value: unknown): value is string {
  return typeof value === "string";
}
function isArrayOf<T>(
  value: unknown,
  guard: (item: unknown) => item is T
): value is T[] {
  return Array.isArray(value) && value.every(guard);
}
const data: unknown = ["a", "b", "c"];
if (isArrayOf(data, isString)) {
  data.forEach(s => s.toUpperCase());  // Type: string[]
}
3. Assertion Functions
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error("Not a string");
  }
}
function processValue(value: unknown) {
  assertIsString(value);
  // value is now typed as string
  console.log(value.toUpperCase());
}
Best Practices
- Use unknownoverany: Enforce type checking
- Prefer interfacefor object shapes: Better error messages
- Use typefor unions and complex types: More flexible
- Leverage type inference: Let TypeScript infer when possible
- Create helper types: Build reusable type utilities
- Use const assertions: Preserve literal types
- Avoid type assertions: Use type guards instead
- Document complex types: Add JSDoc comments
- Use strict mode: Enable all strict compiler options
- Test your types: Use type tests to verify type behavior
Type Testing
// Type assertion tests
type AssertEqual<T, U> =
  [T] extends [U]
    ? [U] extends [T]
      ? true
      : false
    : false;
type Test1 = AssertEqual<string, string>;        // true
type Test2 = AssertEqual<string, number>;        // false
type Test3 = AssertEqual<string | number, string>; // false
// Expect error helper
type ExpectError<T extends never> = T;
// Example usage
type ShouldError = ExpectError<AssertEqual<string, number>>;
Common Pitfalls
- Over-using any: Defeats the purpose of TypeScript
- Ignoring strict null checks: Can lead to runtime errors
- Too complex types: Can slow down compilation
- Not using discriminated unions: Misses type narrowing opportunities
- Forgetting readonly modifiers: Allows unintended mutations
- Circular type references: Can cause compiler errors
- Not handling edge cases: Like empty arrays or null values
Performance Considerations
- Avoid deeply nested conditional types
- Use simple types when possible
- Cache complex type computations
- Limit recursion depth in recursive types
- Use build tools to skip type checking in production
Resources
- TypeScript Handbook: https://www.typescriptlang.org/docs/handbook/
- Type Challenges: https://github.com/type-challenges/type-challenges
- TypeScript Deep Dive: https://basarat.gitbook.io/typescript/
- Effective TypeScript: Book by Dan Vanderkam