TypeScript Fundamentals: Types, Generics and Real-World Patterns · Generics, Utility Types and Real-World TypeScript
Utility, Mapped and Conditional Types
Transform existing types with Partial, Required, Pick, Omit, Record and ReturnType, build your own mapped and conditional types, and use template literal types.
What you'll build
A user-management module that derives all its types from one User definition: an update payload, a public profile, a form-field map, and a type-safe event name generator.
The idea
Instead of rewriting similar types by hand, derive them. TypeScript ships utility types and lets you write your own.
Built-in utilities:
Partial<T>: all properties optional (update payloads).Required<T>: all properties required.Readonly<T>: all properties read-only.Pick<T, "a" | "b">andOmit<T, "a">: keep or drop properties.Record<K, V>: an object with keysKand valuesV.ReturnType<typeof fn>andParameters<typeof fn>: types taken from a function.Awaited<Promise<T>>: the value type of a promise.NonNullable<T>: removesnullandundefined.
Your own building blocks:
- Mapped types loop over keys:
{ [K in keyof T]: boolean }. Add?orreadonlymodifiers, remap keys withas. - Conditional types choose a type by a test:
T extends string ? "text" : "other".infercaptures a part, as inT extends Promise<infer U> ? U : T. - Template literal types build string types:
`on${Capitalize<K>}`.
Reach for these when types would otherwise duplicate each other, and stop when a type becomes hard to read.
The code
interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
}
// Built-in utilities
type UserUpdate = Partial<Omit<User, "id" | "createdAt">>;
type PublicUser = Pick<User, "id" | "name" | "role">;
type RoleCounts = Record<User["role"], number>;
function applyUpdate(user: User, update: UserUpdate): User {
return { ...user, ...update };
}
const user: User = { id: 1, name: "Ada", email: "[email protected]", role: "editor", createdAt: new Date(0) };
const updated = applyUpdate(user, { role: "admin" });
const profile: PublicUser = { id: updated.id, name: updated.name, role: updated.role };
console.log(profile);
const counts: RoleCounts = { admin: 1, editor: 0, viewer: 0 };
console.log(counts);
// Mapped type: a form-field map where every property becomes a string
type FormFields<T> = { [K in keyof T]: string };
const form: FormFields<Pick<User, "name" | "email">> = { name: "Ada", email: "[email protected]" };
console.log(form);
// Mapped type with key remapping and a template literal type
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
type UserGetters = Getters<Pick<User, "name" | "role">>;
const getters: UserGetters = { getName: () => user.name, getRole: () => user.role };
console.log(getters.getName(), getters.getRole());
// Conditional type with infer
type ElementOf<T> = T extends (infer U)[] ? U : T;
type A = ElementOf<string[]>; // string
type B = ElementOf<number>; // number
const a: A = "text";
const b: B = 42;
console.log(a, b);
// Types taken from a function
function makeTodo(title: string) {
return { title, done: false, tags: [] as string[] };
}
type Todo = ReturnType<typeof makeTodo>;
const todo: Todo = makeTodo("Learn mapped types");
console.log(todo);
// @ts-expect-error: id is not allowed in an update
applyUpdate(user, { id: 2 });
Output
{ id: 1, name: 'Ada', role: 'admin' }
{ admin: 1, editor: 0, viewer: 0 }
{ name: 'Ada', email: '[email protected]' }
Ada editor
text 42
{ title: 'Learn mapped types', done: false, tags: [] }Common mistakes
- Using
Partialfor everything. APartial<User>allows an empty object; if some fields are required, combinePickandPartial. - Writing a conditional type when a union or overload is simpler. Conditional types are for relationships between types.
- Building unreadable type puzzles. If a teammate cannot read it, add a name, a comment, or use a simpler design.
- Expecting types to validate runtime data.
Partial<User>does nothing for JSON arriving from a network call; validate it with code. - Forgetting
string & Kwhen usingCapitalizeonkeyof T, because keys can be symbols or numbers too.
Key points
- Derive types with
Partial,Required,Readonly,Pick,Omit,Record,ReturnTypeandAwaited. - Mapped types (
{ [K in keyof T]: ... }) transform every property;asremaps keys. - Conditional types (
T extends U ? X : Y) withinferextract parts of types. - Template literal types build string types such as
getNamefromname.
Further reading: The Handbook: Utility Types and Creating Types from Types.
Practice questions
How would you type an update payload where every User field except id is optional?
Answer
Omit the id and make the rest optional: Partial<Omit<User, "id">>. Omit removes id, then Partial makes the remaining properties optional.
What does `T extends (infer U)[] ? U : T` do?
Answer
If T is an array type, it extracts the element type as U; otherwise it leaves T unchanged. infer declares a type variable that the compiler fills in when the pattern matches.
Open in the app to mark lessons complete, take notes and track your progress.