What youβll learn: Generics (the most powerful TypeScript feature), built-in utility types, type narrowing, and mapped types. These are the concepts youβll encounter constantly in real TypeScript codebases.
Section 1 β Generics
Generics let you write functions and classes that work with any type β but still enforce type safety.
The Problem Without Generics
// Without generics β you lose type info:
function identity(value: any): any {
return value;
}
const result = identity("hello"); // result is 'any' β TypeScript can't help you!
// Or you'd write a separate function for each type:
function identityString(value: string): string { return value; }
function identityNumber(value: number): number { return value; }
// This doesn't scale!
Generic Functions
// <T> is a type parameter β like a variable, but for types
function identity<T>(value: T): T {
return value;
}
// TypeScript infers T:
const s = identity("hello"); // T = string, s is string
const n = identity(42); // T = number, n is number
const arr = identity([1, 2, 3]); // T = number[], arr is number[]
// Explicit type argument:
const s2 = identity<string>("hello"); // same result, explicit
Generic with Arrays
// First element of any array type:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first([1, 2, 3]); // returns number | undefined
first(["a", "b", "c"]); // returns string | undefined
// Map any array to another type:
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
mapArray([1, 2, 3], n => n * 2); // number[]
mapArray(["a", "b"], s => s.length); // number[]
mapArray([1, 2, 3], n => n.toString()); // string[]
Generic Interfaces
// A response wrapper used in all APIs:
interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
timestamp: string;
}
// Use it with any data type:
const fleetResponse: ApiResponse<Fleet> = {
data: { fleetId: "RIPE-NA", region: "us-east-1" },
success: true,
timestamp: new Date().toISOString()
};
const listResponse: ApiResponse<Fleet[]> = {
data: [fleet1, fleet2],
success: true,
timestamp: new Date().toISOString()
};
// A generic repository interface:
interface Repository<T, ID> {
findById(id: ID): Promise<T | null>;
save(entity: T): Promise<T>;
delete(id: ID): Promise<void>;
findAll(): Promise<T[]>;
}
class FleetRepository implements Repository<Fleet, string> {
async findById(id: string): Promise<Fleet | null> { /* ... */ }
async save(fleet: Fleet): Promise<Fleet> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
async findAll(): Promise<Fleet[]> { /* ... */ }
}
Generic Constraints
Restrict what types T can be:
// T must have a 'length' property:
function logLength<T extends { length: number }>(value: T): T {
console.log(`Length: ${value.length}`);
return value;
}
logLength("hello"); // OK β string has length
logLength([1, 2, 3]); // OK β array has length
logLength(42); // Error! β number has no length
// T must be a key of object U:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const fleet = { fleetId: "RIPE-NA", tpm: 50000, region: "us-east-1" };
getProperty(fleet, "fleetId"); // returns string
getProperty(fleet, "tpm"); // returns number
getProperty(fleet, "active"); // Error! 'active' is not a key of fleet
// Multiple constraints:
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
Section 2 β Built-in Utility Types
TypeScript has many built-in helpers to transform types:
Partial<T> β all properties optional
interface Fleet {
fleetId: string;
region: string;
tpm: number;
isActive: boolean;
}
type FleetUpdate = Partial<Fleet>;
// Same as: { fleetId?: string; region?: string; tpm?: number; isActive?: boolean }
// Use case: update functions only need some fields
async function updateFleet(id: string, updates: Partial<Fleet>): Promise<Fleet> {
const current = await getFleet(id);
return { ...current, ...updates };
}
updateFleet("RIPE-NA", { tpm: 60000 }); // OK β only updating tpm
updateFleet("RIPE-NA", { region: "EU", tpm: 0 }); // OK β updating two fields
Required<T> β all properties required
type PartialConfig = {
host?: string;
port?: number;
timeout?: number;
};
type FullConfig = Required<PartialConfig>;
// { host: string; port: number; timeout: number }
// All properties are now required
Readonly<T> β all properties read-only
interface Config {
host: string;
port: number;
}
const config: Readonly<Config> = { host: "localhost", port: 3000 };
config.port = 4000; // Error! Cannot assign to 'port' because it is a read-only property
// Deep readonly (TypeScript doesn't have this built-in, but you can make it):
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
Record<K, V> β object with specific key and value types
// Object where keys are strings and values are numbers:
const tpmByRegion: Record<string, number> = {
"us-east-1": 50000,
"eu-west-1": 30000,
"us-west-2": 20000
};
// Object where keys are from a union type:
type Region = "NA" | "EU" | "FE";
const capacityByRegion: Record<Region, number> = {
NA: 100,
EU: 80,
FE: 60
// TypeScript will error if you miss any key!
};
Pick<T, K> β select subset of properties
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Only expose safe fields (no password):
type PublicUser = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }
function getUserProfile(id: number): Promise<PublicUser> {
// TypeScript ensures password is never returned
}
Omit<T, K> β remove properties
// Same as Pick but you say what to REMOVE:
type PublicUser2 = Omit<User, "password">;
// { id: number; name: string; email: string; createdAt: Date }
// Create input type (no auto-generated fields):
type CreateUserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; password: string }
Exclude<T, U> and Extract<T, U> β work on union types
type AllStatus = "pending" | "active" | "completed" | "cancelled" | "archived";
// Remove specific values from union:
type ActiveStatus = Exclude<AllStatus, "archived" | "cancelled">;
// "pending" | "active" | "completed"
// Keep only specific values:
type TerminalStatus = Extract<AllStatus, "completed" | "cancelled" | "archived">;
// "completed" | "cancelled" | "archived"
NonNullable<T> β remove null and undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string
function processIfExists<T>(value: T | null | undefined): NonNullable<T> | null {
if (value == null) return null;
return value as NonNullable<T>;
}
ReturnType<T> and Parameters<T> β extract function types
async function getFleet(fleetId: string, eventId: string): Promise<Fleet> { /* ... */ }
type GetFleetReturn = ReturnType<typeof getFleet>; // Promise<Fleet>
type GetFleetParams = Parameters<typeof getFleet>; // [string, string]
// Useful when you don't control the function's type:
function wrapWithLogging<T extends (...args: any[]) => any>(
fn: T
): (...args: Parameters<T>) => ReturnType<T> {
return function(...args) {
console.log("Calling with:", args);
return fn(...args);
};
}
Awaited<T> β unwrap Promise types
type FleetPromise = Promise<Fleet>;
type FleetValue = Awaited<FleetPromise>; // Fleet (not Promise<Fleet>)
// Useful with ReturnType of async functions:
async function fetchFleet(): Promise<Fleet> { /* ... */ }
type FetchResult = Awaited<ReturnType<typeof fetchFleet>>; // Fleet
Section 3 β Type Narrowing
TypeScript narrows the type inside conditional blocks:
typeof narrowing
function processId(id: string | number): string {
if (typeof id === "string") {
return id.toUpperCase(); // TypeScript knows id is string here
}
return id.toString(); // TypeScript knows id is number here
}
instanceof narrowing
function handleError(error: Error | string): string {
if (error instanceof Error) {
return error.message; // TypeScript knows it's Error
}
return error; // TypeScript knows it's string
}
in operator narrowing
interface Fleet { fleetId: string; tpm: number; }
interface Service { serviceId: string; email: string; }
function processEntity(entity: Fleet | Service): void {
if ("fleetId" in entity) {
// TypeScript knows entity is Fleet here
console.log(entity.tpm);
} else {
// TypeScript knows entity is Service here
console.log(entity.email);
}
}
Discriminated Unions
The most powerful pattern β add a common property to distinguish types:
type SuccessResult = {
status: "success"; // discriminant property
data: Fleet;
};
type ErrorResult = {
status: "error"; // same property, different value
error: string;
code: number;
};
type Result = SuccessResult | ErrorResult;
function handleResult(result: Result): void {
switch (result.status) { // switch on the discriminant
case "success":
console.log(result.data.fleetId); // TypeScript knows it's SuccessResult
break;
case "error":
console.log(result.error); // TypeScript knows it's ErrorResult
break;
}
}
Custom Type Guards
// Type guard function β returns a boolean AND tells TypeScript the type
function isFleet(entity: unknown): entity is Fleet {
return (
typeof entity === "object" &&
entity !== null &&
"fleetId" in entity &&
"tpm" in entity
);
}
// Usage:
const data: unknown = fetchSomething();
if (isFleet(data)) {
// TypeScript knows data is Fleet here
console.log(data.tpm);
}
Exhaustiveness Checking
Ensure every case is handled:
type Shape = "circle" | "rectangle" | "triangle";
function getArea(shape: Shape, ...dims: number[]): number {
switch (shape) {
case "circle": return Math.PI * dims[0] ** 2;
case "rectangle": return dims[0] * dims[1];
case "triangle": return 0.5 * dims[0] * dims[1];
default:
// This makes TypeScript error if we add a new Shape but forget to handle it:
const _exhaustiveCheck: never = shape;
throw new Error(`Unhandled shape: ${shape}`);
}
}
Section 4 β Mapped Types
Create new types by transforming each property of an existing type:
// Make all properties optional (this is what Partial<T> does):
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties readonly (this is what Readonly<T> does):
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Nullify all properties:
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type NullableFleet = Nullable<Fleet>;
// { fleetId: string | null; tpm: number | null; ... }
// Custom mapping β add a "changed" boolean for each field:
type WithChanges<T> = {
[K in keyof T]: {
value: T[K];
changed: boolean;
};
};
Section 5 β keyof and Indexed Access Types
interface Fleet {
fleetId: string;
region: string;
tpm: number;
isActive: boolean;
}
// keyof β get the union of all property names as string literals
type FleetKey = keyof Fleet; // "fleetId" | "region" | "tpm" | "isActive"
// Indexed access β get the type of a specific property
type FleetTpm = Fleet["tpm"]; // number
type FleetId = Fleet["fleetId"]; // string
// Combine with keyof:
type FleetValue = Fleet[keyof Fleet]; // string | number | boolean (all possible value types)
// Generic type-safe property getter:
function getFleetProperty<T, K extends keyof T>(fleet: T, key: K): T[K] {
return fleet[key]; // TypeScript knows the exact return type
}
const id = getFleetProperty(fleet, "fleetId"); // TypeScript knows: string
const tpm = getFleetProperty(fleet, "tpm"); // TypeScript knows: number
Section 6 β Template Literal Types
// Combine string literals like template strings:
type EventType = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventType>}`;
// "onClick" | "onFocus" | "onBlur"
// Create API endpoint types:
type Entity = "fleet" | "service" | "event";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = `${HttpMethod}:/${Entity}`;
// "GET:/fleet" | "GET:/service" | "GET:/event" | "POST:/fleet" | ...
// Type-safe CSS property names:
type Side = "top" | "right" | "bottom" | "left";
type Spacing = `margin${Capitalize<Side>}`;
// "marginTop" | "marginRight" | "marginBottom" | "marginLeft"
Section 7 β Real-World TypeScript Patterns
API client with generics
interface ApiClient {
get<T>(path: string): Promise<ApiResponse<T>>;
post<T>(path: string, body: unknown): Promise<ApiResponse<T>>;
put<T>(path: string, body: unknown): Promise<ApiResponse<T>>;
delete(path: string): Promise<void>;
}
class HttpClient implements ApiClient {
constructor(private baseUrl: string) {}
async get<T>(path: string): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${path}`);
return response.json();
}
async post<T>(path: string, body: unknown): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
return response.json();
}
// ...
}
// Usage β TypeScript knows the return types:
const client = new HttpClient("https://api.example.com");
const fleet = await client.get<Fleet>("/fleet/RIPE-NA"); // ApiResponse<Fleet>
const user = await client.post<User>("/users", newUser); // ApiResponse<User>
Error handling with typed errors
type AppError =
| { type: "not_found"; message: string; id: string }
| { type: "unauthorized"; message: string }
| { type: "validation"; message: string; fields: string[] }
| { type: "server_error"; message: string; code: number };
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: AppError };
async function getFleet(id: string): Promise<Result<Fleet>> {
try {
const fleet = await db.find(id);
if (!fleet) {
return { ok: false, error: { type: "not_found", message: "Fleet not found", id } };
}
return { ok: true, value: fleet };
} catch (err) {
return { ok: false, error: { type: "server_error", message: "DB error", code: 500 } };
}
}
// Caller handles all cases:
const result = await getFleet("RIPE-NA");
if (result.ok) {
console.log(result.value.tpm); // TypeScript knows it's Fleet
} else {
switch (result.error.type) {
case "not_found": console.log("ID:", result.error.id); break;
case "validation": console.log("Fields:", result.error.fields); break;
// ...
}
}
Section 8 β Quick Reference
Utility Types:
Partial<T> All properties optional
Required<T> All properties required
Readonly<T> All properties read-only
Record<K, V> Object with key type K, value type V
Pick<T, "a"|"b"> Keep only specified properties
Omit<T, "a"|"b"> Remove specified properties
Exclude<T, U> Remove from union T values that extend U
Extract<T, U> Keep from union T values that extend U
NonNullable<T> Remove null and undefined
ReturnType<T> Return type of function T
Parameters<T> Parameter tuple of function T
Awaited<T> Unwrap Promise type
Type Narrowing:
typeof x === "string" Narrows to string
instanceof MyClass Narrows to MyClass
"key" in object Narrows to types that have key
Discriminated union switch on status/type/kind property
Custom guard (is) function x(v: T): v is U
Generics:
<T> Type parameter
<T extends Foo> T must extend Foo
<T, K extends keyof T> K must be a key of T
T[K] Type of property K in T
keyof T Union of all property names