TS

Advanced Types

TypeScript · 8 entries

Conditional Types

syntax
type Result = T extends Condition ? TrueType : FalseType;
example
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;    // "yes"
type B = IsString<number>;    // "no"
type C = IsString<"hello">;   // "yes"

// Practical: flatten one level of array
type Flatten<T> = T extends Array<infer Item> ? Item : T;

type Str = Flatten<string[]>;   // string
type Num = Flatten<number>;     // number (not an array, returned as-is)
output
// Conditional types branch on whether T matches a shape

Note Conditional types distribute over naked union type parameters: IsString<string | number> becomes IsString<string> | IsString<number> = "yes" | "no". Wrap in tuple to prevent distribution: [T] extends [string] ? ... : ...

Mapped Types

syntax
type Result = {
  [K in keyof T]: NewType;
};
example
interface UserProfile {
  name: string;
  email: string;
  age: number;
}

// Make every property a getter function
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<UserProfile>;
// {
//   getName: () => string;
//   getEmail: () => string;
//   getAge: () => number;
// }

// Make all properties mutable (remove readonly)
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};
output
// Mapped types transform every property of an existing type

Note Key remapping with 'as' (TS 4.1+) enables renaming keys. Filter out keys by mapping to never: [K in keyof T as T[K] extends Function ? never : K]. The +/- modifiers add or remove readonly and optional (?) markers.

infer Keyword

syntax
type Result = T extends Pattern<infer U> ? U : Fallback;
example
// Extract the resolved type from a Promise
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;

type A = UnwrapPromise<Promise<string>>;  // string
type B = UnwrapPromise<number>;           // number

// Extract function first argument
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type FA = FirstArg<(name: string, age: number) => void>;  // string

// Extract array element type
type ArrayItem<T> = T extends readonly (infer E)[] ? E : never;
type Item = ArrayItem<readonly ["a", "b", "c"]>;  // "a" | "b" | "c"
output
// infer declares a type variable that TypeScript figures out from context

Note infer can only be used inside the extends clause of a conditional type. It captures whatever type fits in that position. Multiple infer clauses in the same conditional are allowed. In union position, infer produces a union; in intersection position (like function params), it produces an intersection.

Recursive Types

syntax
type TreeNode<T> = {
  value: T;
  children: TreeNode<T>[];
};
example
// JSON-compatible type
type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue };

// Deep Readonly
type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

interface NestedConfig {
  db: { host: string; port: number; ssl: { enabled: boolean } };
}

type FrozenConfig = DeepReadonly<NestedConfig>;
// All levels are readonly — db.ssl.enabled cannot be reassigned
output
// Recursive types reference themselves in their definition

Note TypeScript handles direct recursive type aliases since TS 3.7. Earlier versions needed interface workarounds. Be careful with deeply recursive conditional types — the compiler has a recursion depth limit (around 50 levels by default) and will error if exceeded.

Branded / Nominal Types

syntax
type Brand<T, B> = T & { readonly __brand: B };
example
type USD = number & { readonly __brand: "USD" };
type EUR = number & { readonly __brand: "EUR" };

function usd(amount: number): USD {
  return amount as USD;
}

function eur(amount: number): EUR {
  return amount as EUR;
}

function chargeUSD(amount: USD) {
  console.log(`Charging $${amount}`);
}

const price = usd(29.99);
chargeUSD(price);          // OK
// chargeUSD(eur(25.00));  // Error: EUR is not assignable to USD
// chargeUSD(29.99);       // Error: number is not assignable to USD
output
// Branded types prevent accidentally mixing structurally identical types

Note TypeScript uses structural typing — two types with the same shape are interchangeable. Branded types add a phantom property to create nominal (name-based) distinction. The __brand property never exists at runtime; it is purely a compile-time discriminator. Use this for user IDs, currency, coordinates, etc.

Advanced Template Literal Types

syntax
type Result = `${TypeA}${TypeB}`;
example
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIPath = "/users" | "/orders" | "/products";

// Generate all route strings
type APIRoute = `${HTTPMethod} ${APIPath}`;
// "GET /users" | "GET /orders" | "GET /products" | "POST /users" | ... (12 total)

// Extract path params
type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${string}:${infer Param}`
    ? Param
    : never;

type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
output
// Template literals combined with infer enable string parsing at the type level

Note Template literal types combined with conditional types and infer enable powerful string parsing and transformation at compile time. This is the foundation for type-safe routing, ORM query builders, and CSS-in-JS libraries.

satisfies Operator

syntax
const value = expression satisfies Type;
example
type ColorMap = Record<string, string | number[]>;

// Without satisfies: loses specific types
// const colors: ColorMap = { ... } → all values are string | number[]

// With satisfies: validates shape AND keeps specific types
const colors = {
  red: "#ff0000",
  green: [0, 255, 0],
  blue: "#0000ff",
} satisfies ColorMap;

colors.red.toUpperCase();   // OK — TypeScript knows red is string
colors.green.map(c => c);   // OK — TypeScript knows green is number[]
// colors.red.map(c => c);  // Error — string has no .map
output
// satisfies validates compatibility without widening the inferred type

Note Added in TS 4.9. satisfies checks that a value matches a type without changing the inferred type. This gives you the best of both worlds: validation that the shape is correct, plus precise inference of each property. Use it instead of explicit type annotations when you want both safety and specificity.

keyof & typeof Operators

syntax
type Keys = keyof Type;
type ValueType = typeof runtimeValue;
example
interface Product {
  id: string;
  name: string;
  price: number;
}

type ProductKey = keyof Product; // "id" | "name" | "price"

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map(item => item[key]);
}

// typeof: extract type from a runtime value
const defaultSettings = {
  volume: 50,
  brightness: 80,
  darkMode: true,
};

type Settings = typeof defaultSettings;
// { volume: number; brightness: number; darkMode: boolean }
output
// keyof extracts property names; typeof extracts the type of a value

Note keyof works on types; typeof works on values. They are often combined: keyof typeof myObject gives you the union of property names of a runtime object. typeof only works with variables and properties, not with arbitrary expressions like function calls.