TypeScript Blitz - Day 6: Mapped Types & Conditional Types
Master TypeScript's advanced type system - mapped types, conditional types, template literals, and recursive transformations to build powerful type utilities
Goal
After this lesson you’ll create custom type transformations and build your own utility types like a TypeScript wizard.
Theory Part (20 min)
1. Mapped Types - Basic Syntax
// Mapped type syntax: iterate over keys and transform them
type MappedType<T> = {
[K in keyof T]: T[K];
};
// Example: make all properties optional (like Partial)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = MyPartial<User>;
// {
// id?: number;
// name?: string;
// email?: string;
// }
// Make all properties readonly (like Readonly)
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
type ReadonlyUser = MyReadonly<User>;
// {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// }
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type NullableUser = Nullable<User>;
// {
// id: number | null;
// name: string | null;
// email: string | null;
// }
Key concept: [K in keyof T] iterates over all keys of T, and you can transform the value type T[K].
2. Mapped Types with Modifiers
// Add or remove modifiers with + and -
// Remove optional modifier (make required)
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
interface PartialUser {
id?: number;
name?: string;
email?: string;
}
type RequiredUser = Concrete<PartialUser>;
// {
// id: number;
// name: string;
// email: string;
// }
// Remove readonly modifier (make mutable)
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
interface ReadonlyUser {
readonly id: number;
readonly name: string;
}
type MutableUser = Mutable<ReadonlyUser>;
// {
// id: number;
// name: string;
// }
// Combine modifiers
type ReadonlyPartial<T> = {
readonly [K in keyof T]?: T[K];
};
// Add both modifiers
type RequiredMutable<T> = {
-readonly [K in keyof T]-?: T[K];
};
3. Mapped Types with Key Remapping
// Remap keys using 'as' clause (TypeScript 4.1+)
// Add prefix to all keys
type Prefixed<T, Prefix extends string> = {
[K in keyof T as `${Prefix}${string & K}`]: T[K];
};
interface User {
id: number;
name: string;
}
type PrefixedUser = Prefixed<User, "user_">;
// {
// user_id: number;
// user_name: string;
// }
// Add suffix to all keys
type Suffixed<T, Suffix extends string> = {
[K in keyof T as `${string & K}${Suffix}`]: T[K];
};
type SuffixedUser = Suffixed<User, "_field">;
// {
// id_field: number;
// name_field: string;
// }
// Filter keys (exclude certain properties)
type OmitByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? never : K]: T[K];
};
interface Mixed {
id: number;
name: string;
count: number;
active: boolean;
}
type OnlyStrings = OmitByType<Mixed, number>;
// {
// name: string;
// active: boolean;
// }
// Transform keys to uppercase
type Uppercase<T> = {
[K in keyof T as Uppercase<string & K>]: T[K];
};
type UpperUser = Uppercase<User>;
// {
// ID: number;
// NAME: string;
// }
4. Conditional Types - Basic Syntax
// Conditional type syntax: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Practical example: return type based on condition
type ReturnValue<T> = T extends string ? string[] : number[];
type StringResult = ReturnValue<string>; // string[]
type NumberResult = ReturnValue<number>; // number[]
// Multiple 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<42>; // "number"
type T3 = TypeName<true>; // "boolean"
type T4 = TypeName<() => void>; // "function"
5. Conditional Types with infer
// 'infer' keyword extracts types within conditional types
// Extract return type from function
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: "John" };
}
type UserType = GetReturnType<typeof getUser>;
// { id: number; name: string; }
// Extract parameter types
type GetFirstParam<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
function createUser(name: string, age: number) {
return { name, age };
}
type FirstParam = GetFirstParam<typeof createUser>;
// string
// Extract array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Numbers = ArrayElement<number[]>; // number
type Strings = ArrayElement<string[]>; // string
// Extract Promise value type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Value1 = UnwrapPromise<Promise<string>>; // string
type Value2 = UnwrapPromise<number>; // number
// Deep unwrap nested Promises
type DeepUnwrap<T> = T extends Promise<infer U>
? DeepUnwrap<U>
: T;
type Unwrapped = DeepUnwrap<Promise<Promise<Promise<number>>>>;
// number
6. Distributive Conditional Types
// Conditional types distribute over unions
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// Distributes to: ToArray<string> | ToArray<number>
// Result: string[] | number[]
// Without distribution (wrapped in [])
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]
// Practical use: filter union types
type ExtractStrings<T> = T extends string ? T : never;
type OnlyStrings = ExtractStrings<"a" | "b" | 1 | 2 | true>;
// "a" | "b"
// Remove null/undefined from union
type NonNullable<T> = T extends null | undefined ? never : T;
type Clean = NonNullable<string | null | undefined | number>;
// string | number
7. Template Literal Types
// Template literal types (TypeScript 4.1+)
type Greeting = `Hello ${string}`;
let g1: Greeting = "Hello World"; // ✅ OK
let g2: Greeting = "Hello TypeScript"; // ✅ OK
// let g3: Greeting = "Hi World"; // ❌ Error
// Combine with unions
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";
type ColoredSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" |
// "blue-small" | "blue-medium" | "blue-large" |
// "green-small" | "green-medium" | "green-large"
// Event names
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type HoverEvent = EventName<"hover">; // "onHover"
// URL patterns
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Route = `/api/${string}`;
type Endpoint = `${HTTPMethod} ${Route}`;
let endpoint1: Endpoint = "GET /api/users"; // ✅ OK
let endpoint2: Endpoint = "POST /api/products"; // ✅ OK
// Property getters/setters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// }
8. Intrinsic String Manipulation Types
// Built-in string manipulation types
// Uppercase - convert to uppercase
type UppercaseGreeting = Uppercase<"hello">; // "HELLO"
// Lowercase - convert to lowercase
type LowercaseGreeting = Lowercase<"HELLO">; // "hello"
// Capitalize - capitalize first letter
type CapitalizedWord = Capitalize<"hello">; // "Hello"
// Uncapitalize - uncapitalize first letter
type UncapitalizedWord = Uncapitalize<"Hello">; // "hello"
// Practical combinations
type EventHandler<T extends string> = `on${Capitalize<T>}Changed`;
type NameHandler = EventHandler<"name">; // "onNameChanged"
type AgeHandler = EventHandler<"age">; // "onAgeChanged"
// Create setters from getters
type SetterFromGetter<T extends string> =
T extends `get${infer Field}`
? `set${Field}`
: never;
type Setter = SetterFromGetter<"getName">; // "setName"
9. Recursive Conditional Types
// Recursive types for deep transformations
// Deep Partial - make all nested properties optional
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
interface User {
id: number;
name: string;
address: {
street: string;
city: string;
country: {
code: string;
name: string;
};
};
}
type PartialUser = DeepPartial<User>;
// {
// id?: number;
// name?: string;
// address?: {
// street?: string;
// city?: string;
// country?: {
// code?: string;
// name?: string;
// };
// };
// }
// Deep Readonly - make all nested properties readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
// Flatten nested object to dot notation
type Flatten<T, Prefix extends string = ""> = {
[K in keyof T as K extends string
? `${Prefix}${K}`
: never]: T[K] extends object
? Flatten<T[K], `${Prefix}${K & string}.`>
: T[K];
}[keyof T];
type FlatUser = Flatten<User>;
// "id" | "name" | "address.street" | "address.city" |
// "address.country.code" | "address.country.name"
10. Advanced Mapped Type Patterns
// Mapping with value transformation
// Make all properties required and non-nullable
type Concrete<T> = {
[K in keyof T]-?: NonNullable<T[K]>;
};
interface MaybeUser {
id?: number | null;
name?: string | null;
email?: string;
}
type ConcreteUser = Concrete<MaybeUser>;
// {
// id: number;
// name: string;
// email: string;
// }
// Pick by value type
type PickByValueType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
age: number;
email: string;
}
type OnlyNumbers = PickByValueType<Mixed, number>;
// {
// id: number;
// age: number;
// }
type OnlyStrings = PickByValueType<Mixed, string>;
// {
// name: string;
// email: string;
// }
// Create union from object values
type ValueOf<T> = T[keyof T];
interface Config {
timeout: 5000;
retries: 3;
debug: true;
}
type ConfigValue = ValueOf<Config>;
// 5000 | 3 | true
// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type UserWithOptionalEmail = PartialBy<User, "email">;
// {
// id: number;
// name: string;
// email?: string;
// }
// Make specific properties required
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
11. Conditional Type Constraints
// Constrain conditional types with extends
// Ensure T is an object before processing
type KeysOfObject<T> = T extends object ? keyof T : never;
type K1 = KeysOfObject<{ id: number; name: string }>; // "id" | "name"
type K2 = KeysOfObject<string>; // never
// Extract functions from object
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
interface Methods {
save(): void;
load(): void;
name: string;
count: number;
}
type MethodNames = FunctionPropertyNames<Methods>;
// "save" | "load"
// Filter by type and remap
type StringKeys<T> = {
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
type StringPropsOfUser = StringKeys<User>;
// "name" | "email"
// Conditional type with multiple constraints
type ValidateType<T> =
T extends string ? { type: "string"; value: T } :
T extends number ? { type: "number"; value: T } :
T extends boolean ? { type: "boolean"; value: T } :
{ type: "unknown"; value: T };
type V1 = ValidateType<"hello">; // { type: "string"; value: "hello" }
type V2 = ValidateType<42>; // { type: "number"; value: 42 }
12. Building Custom Utility Types
// Combine everything to create powerful utilities
// Update - merge types with override
type Update<T, U> = Omit<T, keyof U> & U;
interface OldUser {
id: number;
name: string;
age: number;
}
type NewUser = Update<OldUser, { age: string; email: string }>;
// {
// id: number;
// name: string;
// age: string; // overridden
// email: string; // added
// }
// Promisify - wrap all methods in Promise
type Promisify<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T[K];
};
interface SyncAPI {
getUser(id: number): User;
saveUser(user: User): void;
version: string;
}
type AsyncAPI = Promisify<SyncAPI>;
// {
// getUser(id: number): Promise<User>;
// saveUser(user: User): Promise<void>;
// version: string; // unchanged
// }
// Strict Omit - error if key doesn't exist
type StrictOmit<T, K extends keyof T> = Omit<T, K>;
// StrictOmit<User, "invalid"> // ❌ Error!
// Omit<User, "invalid"> // ✅ OK (no error)
// Create object from union
type UnionToObject<T extends string> = {
[K in T]: K;
};
type StatusObject = UnionToObject<"idle" | "loading" | "success">;
// {
// idle: "idle";
// loading: "loading";
// success: "success";
// }
// Optional to Required with defaults
type WithDefaults<T, D extends Partial<T>> = {
[K in keyof T]-?: K extends keyof D ? D[K] : T[K];
};
Practice Part (35 min)
Exercise 1: Custom Form Validation Types (15 min)
Build type-safe form validation utilities using mapped and conditional types.
// Base form field types
interface FormField {
value: string;
error?: string;
touched: boolean;
}
// TODO: Create utility types for forms
// 1. FormState<T> - convert object to form state
// Each property becomes a FormField
// Example: { name: string; age: number } -> { name: FormField; age: FormField }
type FormState<T> = /* TODO */;
// 2. FormErrors<T> - extract only error messages
// Example: { name: FormField; age: FormField } -> { name?: string; age?: string }
type FormErrors<T> = /* TODO */;
// 3. FormValues<T> - extract only values
// Example: { name: FormField; age: FormField } -> { name: string; age: string }
type FormValues<T> = /* TODO */;
// 4. RequiredFields<T> - get union of required field names
// Example: { name: string; email?: string } -> "name"
type RequiredFields<T> = /* TODO */;
// TODO: Implement form utilities
interface UserForm {
username: string;
email: string;
age: number;
bio?: string;
}
// Should create form state
type UserFormState = FormState<UserForm>;
// Should extract errors
function getErrors<T>(form: FormState<T>): FormErrors<T> {
// TODO: Extract all error messages
}
// Should extract values
function getValues<T>(form: FormState<T>): FormValues<T> {
// TODO: Extract all values
}
// Should validate required fields
function validateRequired<T>(
values: Partial<T>,
requiredFields: RequiredFields<T>[]
): boolean {
// TODO: Check if all required fields are present
}
// Tests - uncomment after implementation
// const form: UserFormState = {
// username: { value: "john", touched: true },
// email: { value: "john@email.com", touched: true },
// age: { value: "25", touched: false },
// bio: { value: "", touched: false }
// };
// const errors = getErrors(form);
// const values = getValues(form);
Solution:
Click to see solution
interface FormField {
value: string;
error?: string;
touched: boolean;
}
// 1. Convert all properties to FormField
type FormState<T> = {
[K in keyof T]: FormField;
};
// 2. Extract errors (optional strings)
type FormErrors<T> = {
[K in keyof T]?: string;
};
// 3. Extract values from FormField
type FormValues<T> = {
[K in keyof T]: T[K] extends FormField ? string : T[K];
};
// 4. Get required field names (non-optional)
type RequiredFields<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];
// Implementations
interface UserForm {
username: string;
email: string;
age: number;
bio?: string;
}
type UserFormState = FormState<UserForm>;
function getErrors<T>(form: FormState<T>): FormErrors<T> {
const errors: any = {};
for (const key in form) {
if (form[key].error) {
errors[key] = form[key].error;
}
}
return errors;
}
function getValues<T>(form: FormState<T>): FormValues<T> {
const values: any = {};
for (const key in form) {
values[key] = form[key].value;
}
return values;
}
function validateRequired<T>(
values: Partial<T>,
requiredFields: RequiredFields<T>[]
): boolean {
return requiredFields.every(field => {
const value = values[field as keyof T];
return value !== undefined && value !== null && value !== "";
});
}
// Tests
const form: UserFormState = {
username: { value: "john", touched: true },
email: { value: "john@email.com", touched: true, error: "Invalid format" },
age: { value: "25", touched: false },
bio: { value: "", touched: false }
};
const errors = getErrors(form);
console.log(errors); // { email: "Invalid format" }
const values = getValues(form);
console.log(values); // { username: "john", email: "john@email.com", age: "25", bio: "" }
const isValid = validateRequired(values, ["username", "email"]);
console.log(isValid); // true
Exercise 2: API Client Type Generator (12 min)
Create types that automatically generate API client methods from endpoint definitions.
// Endpoint definition
interface Endpoints {
"/users": {
GET: { response: User[] };
POST: { body: { name: string; email: string }; response: User };
};
"/users/:id": {
GET: { response: User };
PUT: { body: Partial<User>; response: User };
DELETE: { response: void };
};
"/posts": {
GET: { query: { page: number; limit: number }; response: Post[] };
POST: { body: { title: string; content: string }; response: Post };
};
}
// TODO: Create utility types
// 1. ExtractMethods<T> - get all HTTP methods from endpoint
// Example: Endpoints["/users"] -> "GET" | "POST"
type ExtractMethods<T> = /* TODO */;
// 2. GetRequestBody<T, M> - get request body for method
// Example: GetRequestBody<Endpoints["/users"], "POST"> -> { name: string; email: string }
type GetRequestBody<T, M extends keyof T> = /* TODO */;
// 3. GetResponse<T, M> - get response type for method
// Example: GetResponse<Endpoints["/users"], "GET"> -> User[]
type GetResponse<T, M extends keyof T> = /* TODO */;
// 4. ApiClient<T> - generate client interface
// Should create methods for each endpoint/method combination
type ApiClient<T extends Record<string, any>> = /* TODO */;
// TODO: Implement API client generator
type Client = ApiClient<Endpoints>;
// Should have methods like:
// client.get("/users"): Promise<User[]>
// client.post("/users", body): Promise<User>
// client.delete("/users/:id"): Promise<void>
// Tests - uncomment after implementation
// const client: Client = {} as Client;
// const users = await client.get("/users"); // User[]
// const user = await client.post("/users", { // User
// name: "John",
// email: "john@example.com"
// });
Solution:
Click to see solution
interface User {
id: number;
name: string;
email: string;
}
interface Post {
id: number;
title: string;
content: string;
}
interface Endpoints {
"/users": {
GET: { response: User[] };
POST: { body: { name: string; email: string }; response: User };
};
"/users/:id": {
GET: { response: User };
PUT: { body: Partial<User>; response: User };
DELETE: { response: void };
};
"/posts": {
GET: { query: { page: number; limit: number }; response: Post[] };
POST: { body: { title: string; content: string }; response: Post };
};
}
// 1. Extract all methods from endpoint
type ExtractMethods<T> = keyof T;
// 2. Get request body type
type GetRequestBody<T, M extends keyof T> =
T[M] extends { body: infer B } ? B : never;
// 3. Get response type
type GetResponse<T, M extends keyof T> =
T[M] extends { response: infer R } ? R : never;
// 4. Generate client interface
type ApiClient<T extends Record<string, any>> = {
get<P extends keyof T>(
path: P,
...args: T[P] extends { GET: { query: infer Q } } ? [query: Q] : []
): Promise<GetResponse<T[P], "GET">>;
post<P extends keyof T>(
path: P,
body: GetRequestBody<T[P], "POST">
): Promise<GetResponse<T[P], "POST">>;
put<P extends keyof T>(
path: P,
body: GetRequestBody<T[P], "PUT">
): Promise<GetResponse<T[P], "PUT">>;
delete<P extends keyof T>(
path: P
): Promise<GetResponse<T[P], "DELETE">>;
};
// Usage
type Client = ApiClient<Endpoints>;
// Mock implementation
const client: Client = {
async get(path, ...args) {
return fetch(path as string).then(r => r.json());
},
async post(path, body) {
return fetch(path as string, {
method: "POST",
body: JSON.stringify(body)
}).then(r => r.json());
},
async put(path, body) {
return fetch(path as string, {
method: "PUT",
body: JSON.stringify(body)
}).then(r => r.json());
},
async delete(path) {
return fetch(path as string, { method: "DELETE" }).then(r => r.json());
}
};
// Type-safe usage
async function example() {
const users = await client.get("/users"); // User[]
const user = await client.post("/users", { // User
name: "John",
email: "john@example.com"
});
const posts = await client.get("/posts", { page: 1, limit: 10 }); // Post[]
await client.delete("/users/:id"); // void
}
Exercise 3: Deep Type Transformations (8 min)
Create recursive type utilities for deep object transformations.
// TODO: Implement deep transformation utilities
// 1. DeepPartial<T> - make all nested properties optional
type DeepPartial<T> = /* TODO */;
// 2. DeepReadonly<T> - make all nested properties readonly
type DeepReadonly<T> = /* TODO */;
// 3. DeepNullable<T> - make all nested properties nullable
type DeepNullable<T> = /* TODO */;
// 4. Paths<T> - create union of all possible paths in dot notation
// Example: { user: { name: string; age: number } } -> "user" | "user.name" | "user.age"
type Paths<T> = /* TODO */;
// Test with nested structure
interface Company {
name: string;
address: {
street: string;
city: string;
country: {
code: string;
name: string;
};
};
employees: {
id: number;
name: string;
role: string;
}[];
}
// Tests - uncomment after implementation
// type PartialCompany = DeepPartial<Company>;
// type ReadonlyCompany = DeepReadonly<Company>;
// type NullableCompany = DeepNullable<Company>;
// type CompanyPaths = Paths<Company>;
// const partial: PartialCompany = {
// name: "Acme"
// // address is optional
// // address.street is optional
// };
// const paths: CompanyPaths = "address.country.code"; // ✅ OK
Solution:
Click to see solution
// 1. Deep Partial - recursively make optional
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends any[]
? T[K]
: DeepPartial<T[K]>
: T[K];
};
// 2. Deep Readonly - recursively make readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends any[]
? readonly T[K]
: DeepReadonly<T[K]>
: T[K];
};
// 3. Deep Nullable - recursively add null
type DeepNullable<T> = {
[K in keyof T]: T[K] extends object
? T[K] extends any[]
? T[K] | null
: DeepNullable<T[K]> | null
: T[K] | null;
};
// 4. Paths - get all dot-notation paths
type Paths<T, Prefix extends string = ""> = {
[K in keyof T]: K extends string
? T[K] extends object
? T[K] extends any[]
? `${Prefix}${K}`
: `${Prefix}${K}` | Paths<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`
: never;
}[keyof T];
// Test structure
interface Company {
name: string;
address: {
street: string;
city: string;
country: {
code: string;
name: string;
};
};
employees: {
id: number;
name: string;
role: string;
}[];
}
// Tests
type PartialCompany = DeepPartial<Company>;
/*
{
name?: string;
address?: {
street?: string;
city?: string;
country?: {
code?: string;
name?: string;
};
};
employees?: { id: number; name: string; role: string; }[];
}
*/
type ReadonlyCompany = DeepReadonly<Company>;
/*
{
readonly name: string;
readonly address: {
readonly street: string;
readonly city: string;
readonly country: {
readonly code: string;
readonly name: string;
};
};
readonly employees: readonly { id: number; name: string; role: string; }[];
}
*/
type NullableCompany = DeepNullable<Company>;
type CompanyPaths = Paths<Company>;
// "name" | "address" | "address.street" | "address.city" |
// "address.country" | "address.country.code" | "address.country.name" | "employees"
const partial: PartialCompany = {
name: "Acme"
// All nested fields are optional
};
const path1: CompanyPaths = "address.country.code"; // ✅ OK
const path2: CompanyPaths = "name"; // ✅ OK
// const path3: CompanyPaths = "invalid"; // ❌ Error
Quick Review - Key Concepts
1. What are mapped types and when to use them?
Answer
Mapped types iterate over keys of a type and transform them.
Syntax:
type MappedType<T> = {
[K in keyof T]: T[K];
};
When to use:
- Creating variations of existing types (optional, readonly, nullable)
- Transforming all properties in consistent way
- Building reusable type utilities
- Key remapping and filtering
Common patterns:
- Property modifiers:
type Optional<T> = { [K in keyof T]?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
- Value transformation:
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Promisify<T> = { [K in keyof T]: Promise<T[K]> };
- Key remapping (TypeScript 4.1+):
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
- Filtering properties:
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
Benefits:
- DRY - don’t repeat type definitions
- Type-safe transformations
- Automatic updates when base type changes
- Composable with other type operations
2. How does the infer keyword work in conditional types?
Answer
infer declares a type variable within a conditional type that TypeScript infers from the matched structure.
Basic pattern:
T extends SomePattern<infer U> ? U : DefaultType
Common use cases:
- Extract function return type:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { id: 1, name: "John" }; }
type User = ReturnType<typeof getUser>; // { id: number; name: string }
- Extract function parameters:
type FirstParam<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
function createUser(name: string, age: number) { }
type Name = FirstParam<typeof createUser>; // string
- Extract array element type:
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Numbers = ArrayElement<number[]>; // number
- Unwrap Promise:
type Awaited<T> = T extends Promise<infer U> ? U : T;
type Value = Awaited<Promise<string>>; // string
- Deep unwrap (recursive):
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type Value = DeepAwaited<Promise<Promise<number>>>; // number
Key points:
infercan only be used in conditional type’sextendsclause- Multiple
inferdeclarations capture different parts - TypeScript infers the most specific type possible
- Commonly used with function types, arrays, and promises
Pattern matching analogy: infer is like pattern matching - “if T matches this pattern, capture this part as U”
3. What are distributive conditional types?
Answer
Distributive conditional types automatically distribute over union types when the checked type is naked (not wrapped).
How it works:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// Distributes to: ToArray<string> | ToArray<number>
// Result: string[] | number[]
Naked vs Non-naked:
// Naked type parameter - distributes
type Naked<T> = T extends any ? T[] : never;
type R1 = Naked<string | number>;
// string[] | number[]
// Non-naked (wrapped in tuple) - doesn't distribute
type NonNaked<T> = [T] extends [any] ? T[] : never;
type R2 = NonNaked<string | number>;
// (string | number)[]
Practical uses:
- Filter union types:
type ExtractStrings<T> = T extends string ? T : never;
type Strings = ExtractStrings<"a" | 1 | "b" | 2>;
// "a" | "b"
- Built-in utilities use distribution:
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type Without = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type Only = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
- Transform union members:
type Wrap<T> = T extends any ? { value: T } : never;
type Wrapped = Wrap<string | number>;
// { value: string } | { value: number }
Prevent distribution:
// Use tuple wrapper
type NoDistribute<T> = [T] extends [any] ? T[] : never;
// Or use identity function
type NoDistribute2<T> = (T extends any ? T : never)[];
When distribution matters:
- Filtering union types
- Transforming each union member independently
- Building type utilities that work with unions
- Understanding built-in utility types
4. How do template literal types work?
Answer
Template literal types use template literal syntax to create string literal types with patterns.
Basic syntax:
type Greeting = `Hello ${string}`;
let g1: Greeting = "Hello World"; // ✅ OK
let g2: Greeting = "Hello TypeScript"; // ✅ OK
// let g3: Greeting = "Hi World"; // ❌ Error
With union types (combinatorial):
type Color = "red" | "blue";
type Size = "small" | "large";
type ColoredSize = `${Color}-${Size}`;
// "red-small" | "red-large" | "blue-small" | "blue-large"
Common patterns:
- Event handlers:
type EventName<T extends string> = `on${Capitalize<T>}`;
type Click = EventName<"click">; // "onClick"
type Hover = EventName<"hover">; // "onHover"
- CSS properties:
type CSSProp = "margin" | "padding";
type Side = "top" | "bottom" | "left" | "right";
type CSSProperty = `${CSSProp}-${Side}`;
// "margin-top" | "margin-bottom" | ... | "padding-right"
- API routes:
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = `${HTTPMethod} /api/${string}`;
let route: Endpoint = "GET /api/users"; // ✅ OK
- Getters/Setters:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User { name: string; age: number; }
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }
String manipulation utilities:
Uppercase<"hello"> // "HELLO"
Lowercase<"HELLO"> // "hello"
Capitalize<"hello"> // "Hello"
Uncapitalize<"Hello"> // "hello"
Benefits:
- Type-safe string patterns
- Automatic combinations with unions
- Better API design (typed string parameters)
- Pattern matching for strings
5. When to use recursive conditional types?
Answer
Recursive conditional types call themselves to handle nested structures.
When to use:
- Deep property transformations:
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
- Flattening nested structures:
type Flatten<T> = T extends any[]
? Flatten<T[number]>
: T extends object
? { [K in keyof T]: Flatten<T[K]> }[keyof T]
: T;
- Deep path generation:
type Paths<T, Prefix extends string = ""> = {
[K in keyof T]: K extends string
? T[K] extends object
? `${Prefix}${K}` | Paths<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`
: never;
}[keyof T];
- Unwrapping nested types:
type DeepAwaited<T> = T extends Promise<infer U>
? DeepAwaited<U>
: T;
type Value = DeepAwaited<Promise<Promise<Promise<number>>>>;
// number
Best practices:
- Add termination condition:
type Deep<T> = T extends object
? { [K in keyof T]: Deep<T[K]> }
: T; // Base case
- Handle special cases:
type DeepPartial<T> = T extends object
? T extends any[] // Check arrays first
? T
: { [K in keyof T]?: DeepPartial<T[K]> }
: T;
- Limit recursion depth (TypeScript has limits):
// TypeScript limits recursion to prevent infinite loops
// Usually around 50 levels deep
- Test with nested structures:
interface Nested {
a: {
b: {
c: {
d: string;
};
};
};
}
type Test = DeepPartial<Nested>;
Common pitfalls:
- Forgetting base case (infinite recursion)
- Not handling arrays specially
- Not handling functions/classes
- Hitting TypeScript’s recursion limit
When NOT to use:
- Simple flat structures
- Performance-critical types
- When built-in utilities suffice
Checklist - What You Should Know After Day 6
- Create mapped types with property transformations
- Use modifiers (readonly, optional) with + and -
- Remap keys using ‘as’ clause
- Write conditional types with extends
- Extract types using infer keyword
- Understand distributive conditional types
- Use template literal types for string patterns
- Apply string manipulation utilities
- Build recursive type transformations
- Combine mapped and conditional types effectively
Quick Reference Card
// Mapped Types
type Mapped<T> = {
[K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// Remove modifiers
type Required<T> = {
[K in keyof T]-?: T[K];
};
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Key remapping
type Prefixed<T> = {
[K in keyof T as `prefix_${string & K}`]: T[K];
};
// Conditional Types
type IsString<T> = T extends string ? true : false;
// With infer
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Awaited<T> = T extends Promise<infer U> ? U : T;
// Template Literals
type EventName<T extends string> = `on${Capitalize<T>}`;
type CombineStrings = `${string}-${string}`;
// String Manipulation
Uppercase<"hello"> // "HELLO"
Lowercase<"HELLO"> // "hello"
Capitalize<"hello"> // "Hello"
Uncapitalize<"Hello"> // "hello"
// Recursive Types
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
Tomorrow: Classes & OOP in TypeScript - master object-oriented programming with types