DNA🔷 TypeScriptUtility Types & Template Literal Types
ðŸĶ–DinosaurTypeScriptUtility TypesTemplate Literals

Utility Types & Template Literal Types

Master every built-in utility type, build powerful custom ones, and harness template literal types for string-level type safety that catches bugs no linter ever could.

Utility Types & Template Literal Types

TypeScript ships with a rich set of utility types, but senior engineers need to go further — building custom utilities that encode domain rules at the type level and using template literal types to make string-based APIs fully type-safe.

Built-In Utility Types Deep Dive

Object Transformation Types

interface User {
  id: string;
  name: string;
  email: string;
  role: "admin" | "user";
  createdAt: Date;
}
 
type PartialUser = Partial<User>;      // all properties optional
type RequiredUser = Required<User>;    // all properties required
type ReadonlyUser = Readonly<User>;    // all properties readonly
 
type UserPreview = Pick<User, "id" | "name">;
// { id: string; name: string }
 
type UserWithoutDates = Omit<User, "createdAt">;
// { id: string; name: string; email: string; role: "admin" | "user" }
 
type RoleMap = Record<User["role"], User[]>;
// { admin: User[]; user: User[] }

How Partial<T> Actually Works

// Built-in implementation
type Partial<T> = { [P in keyof T]?: T[P] };
 
// It maps over every key and adds the `?` modifier
// The `-?` modifier does the inverse (Required):
type Required<T> = { [P in keyof T]-?: T[P] };

Union Manipulation Types

type Role = "admin" | "user" | "guest" | "superadmin";
 
type NonAdmin = Exclude<Role, "admin" | "superadmin">;
// "user" | "guest"
 
type AdminRoles = Extract<Role, "admin" | "superadmin">;
// "admin" | "superadmin"
 
type MaybeString = NonNullable<string | null | undefined>;
// string

These are implemented with distributive conditional types:

type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type NonNullable<T> = T & {};  // simplified in TS 5.x

Function Inspection Types

function createUser(name: string, email: string, role?: string): User {
  // ...
  return {} as User;
}
 
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, email: string, role?: string]
 
type CreateUserReturn = ReturnType<typeof createUser>;
// User
 
class UserService {
  constructor(private db: Database, private logger: Logger) {}
}
 
type ServiceDeps = ConstructorParameters<typeof UserService>;
// [db: Database, logger: Logger]

Awaited<T> — Unwrapping Promises

type A = Awaited<Promise<string>>;                    // string
type B = Awaited<Promise<Promise<number>>>;           // number (recursive!)
type C = Awaited<string | Promise<boolean>>;          // string | boolean
 
// Implementation (simplified)
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

NoInfer<T> (TS 5.4+)

Prevents TypeScript from using a particular parameter for type inference:

function createFSM<S extends string>(config: {
  initial: NoInfer<S>;
  states: S[];
}) {}
 
createFSM({
  initial: "idle",    // ❌ Error if "idle" is not in states
  states: ["idle", "loading", "done"],
});
 
// Without NoInfer, TypeScript would infer S from BOTH initial and states,
// allowing any string in initial. NoInfer forces inference from states only.

Custom Utility Types

DeepPartial<T>

Makes every property optional recursively:

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;
 
interface Config {
  database: {
    host: string;
    port: number;
    credentials: { user: string; password: string };
  };
  features: { darkMode: boolean; beta: boolean };
}
 
type PartialConfig = DeepPartial<Config>;
// Every nested property is optional — perfect for config overrides
const override: PartialConfig = { database: { port: 5433 } };

DeepReadonly<T>

type DeepReadonly<T> = T extends (infer U)[]
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;
 
const state: DeepReadonly<{ users: { name: string }[] }> = {
  users: [{ name: "Ada" }],
};
state.users[0].name = "Bob"; // ❌ Cannot assign to 'name' because it is a read-only property
state.users.push({ name: "Bob" }); // ❌ push does not exist on ReadonlyArray

MutableKeys<T> and RequiredKeys<T>

type IfEquals<X, Y, A, B> =
  (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? A : B;
 
type MutableKeys<T> = {
  [K in keyof T]-?: IfEquals<
    { [Q in K]: T[K] },
    { -readonly [Q in K]: T[K] },
    K,
    never
  >;
}[keyof T];
 
type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];
 
interface Example {
  readonly id: string;
  name: string;
  email?: string;
}
 
type Mutable = MutableKeys<Example>;  // "name" | "email"
type Required = RequiredKeys<Example>; // "id" | "name"

Prettify<T> — Flatten Intersections for Readability

type Prettify<T> = { [K in keyof T]: T[K] } & {};
 
type Ugly = { a: string } & { b: number } & { c: boolean };
// Hover shows: { a: string } & { b: number } & { c: boolean }
 
type Pretty = Prettify<Ugly>;
// Hover shows: { a: string; b: number; c: boolean }

This is a cosmetic utility, but it dramatically improves developer experience when composing complex types.

StrictOmit<T, K> — Omit That Catches Typos

type StrictOmit<T, K extends keyof T> = Omit<T, K>;
 
// Built-in Omit allows any string key — including typos
type Broken = Omit<User, "nme">;      // ✅ No error! "nme" silently ignored
type Safe = StrictOmit<User, "nme">;   // ❌ Error: "nme" not in keyof User

Always prefer StrictOmit — the built-in Omit accepting arbitrary keys is a well-known footgun.

Template Literal Types

Built-In String Manipulation Types

type Upper = Uppercase<"hello">;       // "HELLO"
type Lower = Lowercase<"HELLO">;       // "hello"
type Cap = Capitalize<"hello">;        // "Hello"
type Uncap = Uncapitalize<"Hello">;    // "hello"

Type-Safe Route Paths

type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractParams<Rest>]: string }
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};
 
type UserRouteParams = ExtractParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }
 
function navigate<T extends string>(
  path: T,
  params: ExtractParams<T>
): void {
  // Replace :param with actual values
}
 
navigate("/users/:userId/posts/:postId", {
  userId: "123",
  postId: "456",
}); // ✅
 
navigate("/users/:userId/posts/:postId", {
  userId: "123",
}); // ❌ Missing postId

CSS Property Types

type CSSUnit = "px" | "em" | "rem" | "vh" | "vw" | "%";
type CSSValue = `${number}${CSSUnit}` | "auto" | "inherit";
 
function setWidth(element: HTMLElement, value: CSSValue): void {
  element.style.width = value;
}
 
setWidth(document.body, "100px");  // ✅
setWidth(document.body, "2.5rem"); // ✅
setWidth(document.body, "auto");   // ✅
setWidth(document.body, "big");    // ❌

Event Handler Name Generation

type DOMEventMap = {
  click: MouseEvent;
  scroll: Event;
  keydown: KeyboardEvent;
  resize: UIEvent;
};
 
type EventHandlerName<T extends string> = `on${Capitalize<T>}`;
 
type EventHandlers = {
  [K in keyof DOMEventMap as EventHandlerName<K & string>]: (
    event: DOMEventMap[K]
  ) => void;
};
// {
//   onClick: (event: MouseEvent) => void;
//   onScroll: (event: Event) => void;
//   onKeydown: (event: KeyboardEvent) => void;
//   onResize: (event: UIEvent) => void;
// }

Parsing Dot-Notation Paths

type PathValue<T, Path extends string> =
  Path extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T
      ? PathValue<T[Key], Rest>
      : never
    : Path extends keyof T
      ? T[Path]
      : never;
 
interface AppState {
  user: {
    profile: { name: string; avatar: string };
    settings: { theme: "light" | "dark"; lang: string };
  };
  posts: { id: string; title: string }[];
}
 
type Theme = PathValue<AppState, "user.settings.theme">;  // "light" | "dark"
type Name = PathValue<AppState, "user.profile.name">;      // string
type Wrong = PathValue<AppState, "user.foo">;               // never
 
function get<T, P extends string>(obj: T, path: P): PathValue<T, P> {
  return path.split(".").reduce((acc: any, key) => acc[key], obj);
}
 
const state: AppState = {} as AppState;
const theme = get(state, "user.settings.theme"); // type: "light" | "dark"

Building a Type-Safe SQL Query Builder with Template Literals

type Table = "users" | "posts" | "comments";
type Column<T extends Table> = T extends "users"
  ? "id" | "name" | "email"
  : T extends "posts"
    ? "id" | "title" | "body" | "authorId"
    : "id" | "text" | "postId";
 
type SelectQuery<T extends Table> =
  `SELECT ${Column<T> | "*"} FROM ${T}`;
 
type WhereClause<T extends Table> =
  `WHERE ${Column<T>} = $${string}`;
 
type FullQuery<T extends Table> =
  SelectQuery<T> | `${SelectQuery<T>} ${WhereClause<T>}`;
 
function query<T extends Table>(sql: FullQuery<T>): void {}
 
query("SELECT * FROM users");                         // ✅
query("SELECT name FROM users WHERE email = $email"); // ✅
query("SELECT foo FROM users");                       // ❌ "foo" is not a column
query("SELECT * FROM dogs");                          // ❌ "dogs" is not a table

Composing Utility Types

The real power emerges when you compose utilities:

type FormFields<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};
 
type FormState<T> = {
  values: T;
  errors: Partial<Record<keyof T, string>>;
  touched: Partial<Record<keyof T, boolean>>;
  dirty: boolean;
};
 
type UserFormFields = FormFields<User>;
type UserFormState = FormState<UserFormFields>;
type APIResource<T> = {
  list: () => Promise<T[]>;
  get: (id: string) => Promise<T>;
  create: (data: Omit<T, "id" | "createdAt">) => Promise<T>;
  update: (id: string, data: Partial<Omit<T, "id" | "createdAt">>) => Promise<T>;
  delete: (id: string) => Promise<void>;
};
 
type UserAPI = APIResource<User>;
// Fully typed CRUD operations automatically derived from the User type

Interview Power Moves

  1. "I use StrictOmit over the built-in Omit — the built-in accepts any key string, which means typos silently pass" — shows attention to type safety details.

  2. "Template literal types let me make string-based APIs type-safe without code generation" — demonstrates awareness of the alternative (codegen) and why template literals are often better.

  3. "I implement Prettify<T> in every project — it's zero runtime cost but transforms the developer experience when hovering over complex intersection types."

  4. "The key insight about utility types is that they compose. DeepPartial, StrictOmit, and Prettify together handle 90% of type transformation needs."

  5. "NoInfer from TypeScript 5.4 solved a long-standing inference problem — before it, we had to use workaround patterns like T & {} to prevent inference from certain positions."