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>;
// stringThese 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.xFunction 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 ReadonlyArrayMutableKeys<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 UserAlways 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 postIdCSS 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 tableComposing 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 typeInterview Power Moves
-
"I use
StrictOmitover the built-inOmitâ the built-in accepts any key string, which means typos silently pass" â shows attention to type safety details. -
"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.
-
"I implement
Prettify<T>in every project â it's zero runtime cost but transforms the developer experience when hovering over complex intersection types." -
"The key insight about utility types is that they compose.
DeepPartial,StrictOmit, andPrettifytogether handle 90% of type transformation needs." -
"
NoInferfrom TypeScript 5.4 solved a long-standing inference problem â before it, we had to use workaround patterns likeT & {}to prevent inference from certain positions."