function multiply(a: number, b: number = 1) {
return a * b;
}
multiply(5); // 5
Defaults provide a fallback when the caller omits a parameter.
Rest Parameters
function sum(...values: number[]): number {
return values.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4);
Rest parameters allow variable-length arguments.
The type must be an array type.
Function Return Types
function isEven(n: number): boolean {
return n % 2 === 0;
}
Return types can be inferred, but explicit typing improves clarity.
Anonymous and Arrow Functions
const double = (x: number): number => x * 2;
const logger = function(message: string): void {
console.log(message);
};
Arrow functions inherit this from their surrounding scope.
Anonymous functions are useful for callbacks or inline logic.
Function Overloads
function format(value: number): string;
function format(value: string): string;
function format(value: number | string) {
return `Value: ${value}`;
}
format(10);
format("hello");
Type aliases allow naming reusable object type definitions.
They improve readability and reduce duplication.
Intersection-Based Object Composition
type HasID = { id: number };
type HasName = { name: string };
type User = HasID & HasName;
Intersections combine smaller object fragments into larger, more detailed structures.
Promotes modular design and DRY principles.
Index Signatures (Dynamic Object Keys)
type Scores = {
[player: string]: number;
};
let game: Scores = {
Alice: 10,
Bob: 8,
Charlie: 12
};
Use index signatures when the keys are unknown ahead of time.
Each key must have a value of the specified type.
Excess Property Checks
type User = { id: number; name: string };
let u: User = {
id: 1,
name: "Alice",
// age: 30 ❌ Error: extra property
};
TypeScript checks for unexpected properties when assigning literal objects.
This catches typos and structural mismatches early.
Using in for Object Type Narrowing
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim();
} else {
animal.fly();
}
}
The in operator narrows union object types based on property existence.
Useful for discriminated unions and polymorphic behavior.
Object Type Compatibility
type Animal = { name: string };
type Dog = { name: string; bark: () => void };
let a: Animal = { name: "Spike" };
let d: Dog = { name: "Buddy", bark() {} };
a = d; // ✔ OK (Dog has at least the properties Animal requires)
TypeScript uses structural typing — only property shapes matter.
Extra properties are allowed when assigning larger objects to smaller ones.
Readonly vs Mutable Object Types
type MutablePoint = { x: number; y: number };
type ReadonlyPoint = {
readonly x: number;
readonly y: number;
};
Classes can implement interfaces to guarantee method and property structure.
Provides compile-time contracts for class behavior.
Constructor Type vs Instance Type
class Car {
constructor(public brand: string) {}
}
type CarInstance = Car; // instance type
type CarConstructor = typeof Car; // constructor function type
Car refers to the instance type.
typeof Car refers to the constructor function type.
Useful for dependency injection, factories, or meta-programming.
Private Fields (# syntax)
class Counter {
#count = 0; // hard private, JS-level
increment() {
this.#count++;
}
}
#private fields are enforced by JavaScript itself.
They differ from private, which is enforced by TypeScript only.
Constructor Type vs Instance Type
Introduction
In TypeScript, classes create two types simultaneously:
Instance Type — the type of objects created by new.
Constructor Type — the type of the class constructor function itself.
These two types behave differently and are used in different contexts.
Understanding the distinction is essential for dependency injection, factories, generics, and meta-programming patterns.
Instance Type
class User {
constructor(public id: number, public name: string) {}
}
let u: User = new User(1, "Alice");
User refers to the type of instances created by new User().
This includes all instance properties and methods.
It does not include static members or the constructor signature.
Constructor Type
type UserConstructor = typeof User;
typeof User refers to the constructor function itself.
This includes:
the new (...args) signature
static properties
the constructor's parameter types
Constructor types are useful for passing classes as values.
Example: Comparing Both Types
class Car {
static company = "Tesla";
constructor(public model: string) {}
}
type CarInstance = Car; // instance type
type CarConstructor = typeof Car; // constructor type
Feature
Instance Type (Car)
Constructor Type (typeof Car)
Instance properties
✔ Available
✘ Not available
Instance methods
✔ Available
✘ Not available
Static members
✘ Not available
✔ Available
Constructor signature
✘ No
✔ Yes
Used with new
✘ No
✔ Yes
Using Constructor Types for Factories
function createInstance(ctor: { new (...args: any[]): any }) {
return new ctor();
}
class A { constructor() { console.log("A created"); } }
class B { constructor() { console.log("B created"); } }
createInstance(A);
createInstance(B);
Factories often accept constructor types rather than instances.
TypeScript enforces that the provided argument supports new.
Typing Class Constructors More Strictly
interface UserConstructor {
new (id: number, name: string): { id: number; name: string };
}
function createUser(ctor: UserConstructor) {
return new ctor(1, "Alice");
}
You can define detailed constructor signatures using interfaces.
This is extremely powerful for plugins, DI containers, and testing.
Static Properties Live on the Constructor Type
class Settings {
static version = "1.0";
theme = "light";
}
let s: Settings = new Settings(); // instance type
let C: typeof Settings = Settings; // constructor type
console.log(C.version); // OK
console.log(s.theme); // OK
Static fields belong to the constructor type.
Instance fields belong to the instance type.
Constructor Types and Dependency Injection
function runService(ctor: { new(): Service }) {
const service = new ctor();
service.run();
}
class Service {
run() { console.log("Running service..."); }
}
runService(Service);
DI frameworks frequently accept constructor types to dynamically create instances.
TypeScript ensures correctness via new() signatures.
Constructor Types with Generics
function create<T>(ctor: { new (...args: any[]): T }): T {
return new ctor();
}
class Logger { log() { console.log("log"); } }
const logger = create(Logger);
Generics ensure the returned instance has the correct type.
Used widely in factory abstractions and IoC containers.
A Class Is Both a Value and a Type
class Example {}
type T1 = Example; // instance type
const T2 = Example; // constructor value
Example (in a type position) = the instance type.
Example (in a value position) = the constructor function.
This duality is unique compared to many other languages.
Constructor Type vs Instance Type
class User {
static role = "user-class";
constructor(public id: number, public name: string) {}
greet() {
console.log(`Hello, ${this.name}`);
}
}
// 1) Instance type: "what new User() returns"
let instance: User = new User(1, "Alice");
instance.id; // OK - instance property
instance.name; // OK - instance property
instance.greet(); // OK - instance method
// instance.role; // Error: 'role' is static (on the constructor, not instance)
// 2) Constructor type: "the class itself (function + statics)"
let Ctor: typeof User = User;
Ctor.role; // OK - static property
const other = new Ctor(2, "Bob"); // OK - calling the constructor
// other has the instance type:
other.id; // OK
other.greet(); // OK
// 3) Using constructor type in a factory
function createAndGreet(ctor: typeof User, id: number, name: string) {
const u = new ctor(id, name); // we can 'new' the constructor
u.greet();
}
createAndGreet(User, 3, "Charlie");
User (as a type) describes the instance you get from new User(...) (instance fields + methods, no statics, no constructor).
typeof User (as a type) describes the constructor itself (the class function):
has new (...) so you can construct instances
contains static members like role
does not have instance fields like id or instance methods like greet()
Use User when you are talking about objects (instances), and typeof User when you are passing the class itself around (e.g. factories, DI).
Generics
Introduction
They are written using angle brackets <T>, <T, U> and can be used on:
functions
type aliases
interfaces
classes
Basic Generic Function
function identity<T>(value: T): T {
return value;
}
const a = identity(10); // T = number
const b = identity("hello"); // T = string
T is a type parameter — a placeholder for "some type".
When you call identity, TypeScript infers T from the argument.
The function now returns the same type it receives, without losing information.
Interfaces can be generic and represent generic APIs (e.g. repositories, services, collections).
Repository<User> is a concrete interface where T = User.
Generic Classes
class Stack<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
const strStack = new Stack<string>();
strStack.push("a");
strStack.push("b");
Generic classes are containers that can store values of a configurable type.
The same class Stack is reused for many types.
Constrained Generics with extends
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
getLength("hello"); // ✔ string has length
getLength([1, 2, 3]); // ✔ array has length
// getLength(123); // ❌ number has no length
T extends { length: number } restricts T to types that have a length property.
If you pass a type that doesn't satisfy the constraint, TypeScript errors.
Constraints are important for using properties safely in generic code.
Keyof and Indexed Access with Generics
type User = {
id: number;
name: string;
};
type UserKey = keyof User; // "id" | "name"
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const u: User = { id: 1, name: "Alice" };
const idValue = getProp(u, "id");
const nameValue = getProp(u, "name");
// getProp(u, "age"); // ❌ Error: "age" not in "id" | "name"
keyof T produces a union of property names of T.
K extends keyof T ensures key is a valid property name.
T[K] is an indexed access type — "type of property K on T".
This pattern is the basis of many advanced utility types.
Default Type Parameters
type ApiResponse<T = unknown> = {
data: T;
error: string | null;
};
const r1: ApiResponse = {
data: 123, // T = unknown, but still typed
error: null
};
const r2: ApiResponse<{ id: number }> = {
data: { id: 1 },
error: null
};
<T = unknown> gives T a default type when the user does not specify one.
Similar to default values for function parameters.
Useful for APIs where the generic is often omitted.
Generic Utility Types (Built-in)
type User = {
id: number;
name: string;
email?: string;
};
type ReadonlyUser = Readonly<User>;
type PartialUser = Partial<User>;
type UserWithoutEmail = Omit<User, "email">;
TypeScript ships with many generic utility types (all defined using generics):
Array<T> and Promise<T> are generic types from the standard library.
Promise<User> means "a promise that eventually yields a User".
Generic Constraints with Multiple Parameters
function merge<A extends object, B extends object>(a: A, b: B): A & B {
return { ...a, ...b };
}
const merged = merge(
{ id: 1, name: "Alice" },
{ active: true }
);
// type of merged: { id: number; name: string } & { active: boolean }
Here, A and B must be objects.
The result type is an intersection A & B, containing all properties from both arguments.
Generics + intersections give powerful composition of object types.
Generic "Shape-Preserving" Functions
function mapValues<T, U>(
obj: T,
fn: (value: T[keyof T]) => U
): { [K in keyof T]: U } {
const result: any = {};
for (const key in obj) {
result[key] = fn(obj[key]);
}
return result;
}
const original = { a: 1, b: 2, c: 3 };
const doubled = mapValues(original, v => v * 2);
// type of doubled: { a: number; b: number; c: number }
This example uses:
generic parameters T, U
keyof T and indexed access
a mapped type { [K in keyof T]: U }
Generics allow TypeScript to preserve the exact shape of objects while transforming their value types.
When to Use Generics
Situation
Example
Why Generics?
Reusable container
Stack<T>, Box<T>
Same logic for many element types
Reusable API
Repository<T>
One interface for many domain models
Transforming types
Readonly<T>, Partial<T>
Programmatic type manipulation
Safe property access
getProp<T, K>(obj, key)
Prevent typos and invalid keys
If you find yourself writing the same function/type for User, Product, Order, etc., it is often a good candidate for generics.
The keyof Type Operator
Introduction
keyof is a TypeScript type operator that produces a union of the property names of a type.
It is used only in type positions (not at runtime).
Together with indexed access types (T[K]) and generics, keyof is one of the core building blocks for advanced typing.
Basic Usage with Object Types
type User = {
id: number;
name: string;
active: boolean;
};
type UserKeys = keyof User;
// type UserKeys = "id" | "name" | "active"
keyof User becomes a union of all property names on User.
Here, UserKeys can only be "id", "name", or "active".
Using keyof to Type-Safe Property Access
type User = {
id: number;
name: string;
};
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const u: User = { id: 1, name: "Alice" };
const id = getProp(u, "id"); // type: number
const name = getProp(u, "name"); // type: string
// getProp(u, "age"); // ❌ Error: "age" not in "id" | "name"
K extends keyof T ensures that key is a valid property name of T.
T[K] gives the type of that property, fully type-safe.
keyof with Index Signatures
type Scores = {
[player: string]: number;
};
type ScoreKey = keyof Scores;
// type ScoreKey = string | number
For string index signatures, keyof usually becomes string | number because JS object keys are treated as strings internally.
This is why you often see Record<string, T> together with keyof.
keyof with Arrays and Tuples
type Numbers = number[];
type NumbersKey = keyof Numbers;
// type NumbersKey = number | "length" | "push" | "pop" | ...
type Point = [number, number];
type PointKey = keyof Point;
// type PointKey = "0" | "1" | "length" | ...
For normal arrays, keyof includes numeric indexes (number) and array methods like "length", "push", etc.
For tuples, keyof includes string literal indices like "0", "1", plus common array members.
Because of this, you usually use number (or explicit indices) to index tuples instead of keyof directly.
keyof and Unions of Object Types
type Cat = { meow: () => void; name: string };
type Dog = { bark: () => void; name: string };
type Pet = Cat | Dog;
type PetKeys = keyof Pet;
// type PetKeys = "name" | "meow" | "bark"
For unions of object types, keyof gives the union of all keys.
Here, Pet might have "meow" (for Cat) or "bark" (for Dog), and always "name".
keyof and Intersections
type A = { id: number; name: string };
type B = { name: string; active: boolean };
type AB = A & B;
type ABKeys = keyof AB;
// type ABKeys = "id" | "name" | "active"
For intersections, the resulting type has all properties, so keyof gives the union of those keys.
Intersections are often used with keyof to model combined domains.
keyof Special Cases: any and never
type KAny = keyof any; // string | number | symbol
type KNever = keyof never; // never
keyof any is defined as string | number | symbol (all possible property key types).
keyof never is never, because there is no value of type never, and thus no keys.
Combining keyof with Indexed Access Types
type User = {
id: number;
name: string;
active: boolean;
};
type UserKey = keyof User; // "id" | "name" | "active"
type UserValue = User[UserKey];
// type UserValue = number | string | boolean
User[UserKey] means "the union of the types of all properties of User".
This pattern is useful when you want "any value from this object type".
Building Utility Types with keyof
type User = {
id: number;
name: string;
email?: string;
};
type PickProps<T, K extends keyof T> = {
[P in K]: T[P];
};
type UserBasic = PickProps<User, "id" | "name">;
// type UserBasic = { id: number; name: string; }
K extends keyof T ensures we only pick existing keys.
[P in K]: T[P] (a mapped type) uses keyof and indexed access to build new object shapes.
Many built-in utilities like Pick, Omit, Readonly, and Partial are defined using keyof.
Using keyof to Constrain APIs
type Settings = {
theme: "light" | "dark";
language: "en" | "de";
pageSize: number;
};
function updateSetting<K extends keyof Settings>(
key: K,
value: Settings[K]
) {
// ...
}
updateSetting("theme", "dark"); // ✔ OK
updateSetting("pageSize", 20); // ✔ OK
// updateSetting("language", "fr"); // ❌ Error: "fr" not assignable to "en" | "de"
keyof lets the function accept only valid setting names.
Settings[K] forces the value to match the exact type for that key.
This pattern is extremely common for config APIs, Redux-like stores, or form updaters.
Indexed Access Types (T[K])
Indexed access types let you look up the type of a property by its key, using the syntax T[K].
You can think of T as an object type, and K as a property name (or union of names).
Indexed access types are pure type-level operations, they do not exist at runtime.
Basic Example: Single Property
type User = {
id: number;
name: string;
active: boolean;
};
type UserIdType = User["id"]; // number
type UserNameType = User["name"]; // string
Using Unions as Index Keys
type User = {
id: number;
name: string;
active: boolean;
};
type IdOrName = User["id" | "name"]; // number | string
Combining with keyof: All Property Values
type User = {
id: number;
name: string;
active: boolean;
};
type UserKey = keyof User; // "id" | "name" | "active"
type UserValue = User[UserKey]; // number | string | boolean
Working with Arrays and Tuples
type Numbers = number[];
type Element = Numbers[number];
// type Element = number
type Point = [number, number, number];
type PointIndex = keyof Point;
// type PointIndex = "0" | "1" | "2" | "length" | ...
type PointElement = Point[number];
// type PointElement = number
For arrays, T[number] gives "element type".
For tuples, T[number] gives a union of all element types.
This is how built-in helpers like ReturnType and Parameters work internally.
Nested Indexed Access
type Product = {
id: number;
details: {
name: string;
price: number;
};
};
type ProductDetails = Product["details"];
// { name: string; price: number; }
type ProductPrice = Product["details"]["price"];
// number
Indexed access can be chained to drill into nested types.
This is useful when you want types for deep properties without manually rewriting them.
Indexed Access with Generics
type GetPropType<T, K extends keyof T> = T[K];
type User = {
id: number;
name: string;
};
type UserIdType = GetPropType<User, "id">; // number
type UserNameType = GetPropType<User, "name">; // string
Shape-Preserving Functions with T[K]
function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map(k => obj[k]);
}
type User = {
id: number;
name: string;
active: boolean;
};
const u: User = { id: 1, name: "Alice", active: true };
const values = pluck(u, ["id", "name"]);
// type of values: (number | string)[]
T[K] represents the type of elements in the returned array.
The compiler knows that:
keys contains valid property names of T
and each element of values is the type of the corresponding property
Using Indexed Access in Utility Types
type PickProps<T, K extends keyof T> = {
[P in K]: T[P];
};
type User = {
id: number;
name: string;
email: string;
};
type BasicUser = PickProps<User, "id" | "name">;
// { id: number; name: string; }
Mapped types often use T[P] on the right-hand side to copy property types.
This pattern is the basis for built-in utilities like Pick, Omit, Readonly, and Partial.
Extracting the Type of a Method
type Service = {
start(): void;
stop(code: number): void;
};
type StartType = Service["start"]; // () => void
type StopType = Service["stop"]; // (code: number) => void
Indexed access works for methods as well: you get the function type.
This is handy when you want to re-use or transform method signatures.
Value Type of a Dictionary
type Dictionary = Record<string, number>;
type DictValue = Dictionary[string];
// type DictValue = number
For "map-like" types, T[string] or T[number] gives you the value type.
This is frequently used in libraries dealing with key-value objects.
Indexed Access and Readonly Transformations
type ReadonlyObject<T> = {
readonly [K in keyof T]: T[K];
};
type User = {
id: number;
name: string;
};
type ReadonlyUser = ReadonlyObject<User>;
// { readonly id: number; readonly name: string; }
T[K] copies the original property type from T into the new object.
Here only the modifier (readonly) changes — the underlying property types stay exactly the same.
Index Signatures for Dynamic String Keys
Sometimes you don't know the exact property names in advance, but you do know that all keys are strings and all values share the same type.
In that case, you can declare an index signature such as [key: string]: string to express:
"any string can be used as a property name"
"each property must store a string"
Index signatures are the TypeScript way to model dictionary/map objects, including keys with spaces, dashes, or other special characters.
type StringMap = {
// NOTE: the name `key` here is arbitrary, you can use any identifiers here.
[key: string]: string;
};
const m: StringMap = {
"user name": "Alice",
"user-email": "a@example.com",
};
const a = m["user name"]; // string
const b = m["user-email"]; // string
// const c = m["age"]; // string | undefined (runtime)
Any valid JavaScript string can be used as a key: "user name", "user-email", "x.y.z", etc.
Because such keys are not valid identifiers, you must use bracket notation:
m["user name"] ✔ valid
m.user name ✘ syntax error
The index signature ensures that every property value inside a StringMap is always a string, regardless of how unusual the key is.
You can also express this pattern using the built-in utility: Record<string, string>.
Conditional Types
Conditional types allow TypeScript to express logic inside the type system.
The syntax resembles a JavaScript ternary, but applies entirely at compile time:
T extends U ? X : Y
It means: "If T is assignable to U, then the resulting type is X, otherwise Y."
Basic Syntax
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
If T fits the constraint string, the type becomes "yes".
Otherwise it becomes "no".
Conditional Types with Generics
type Boxed<T> =
T extends number ? { value: number } :
T extends string ? { value: string } :
{ value: T };
type X = Boxed<number>; // { value: number }
type Y = Boxed<string>; // { value: string }
type Z = Boxed<boolean>; // { value: boolean }
Generic conditional types allow you to branch based on the shape or category of T.
This is often used in API schemas or serialization libraries.
Distributive Conditional Types
When the checked type T is a union, conditional types automatically distribute over each member.
This is one of the most powerful and surprising features of the TypeScript type system.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
// A = string[] | number[]
Each union member is processed individually, producing a union of results.
This is useful for mapping or filtering unions.
Filtering Unions
type ExtractString<T> = T extends string ? T : never;
type R = ExtractString<string | number | boolean>;
// R = string
Conditional types can "pick out" parts of a union.
never removes branches, so only string remains.
Excluding Types
type ExcludeNumber<T> = T extends number ? never : T;
type R = ExcludeNumber<string | number | boolean>;
// R = string | boolean
Conditional types can remove specific members from a union.
This is exactly how the built-in Exclude<T, U> utility is implemented.
Inferring Types Using infer
Conditional types can extract (infer) parts of a type automatically.
infer introduces a type variable whose type is deduced from the structure of the input.
type ReturnTypeOf<T> =
T extends (...args: any[]) => infer R
? R
: never;
function foo() {
return { id: 1, name: "Alice" };
}
type R = ReturnTypeOf<typeof foo>;
// { id: number; name: string }
infer R captures the return type of the function.
This mechanism powers many of TypeScript's built-in helpers.
Inferring Tuple and Array Element Types
type ElementOf<T> =
T extends (infer U)[] ? U : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<number[]>; // number
infer U extracts the element type of an array.
Useful for generic collections and serialization utilities.
Inferring Function Parameters
type FirstArg<T> =
T extends (arg: infer A, ...rest: any[]) => any
? A
: never;
type F = FirstArg<(x: number, y: string) => void>;
// number
You can pattern-match function signatures and pull out their components.
Conditional Types with readonly and Mutability Checks
type IsReadonlyArray<T> = T extends readonly any[] ? true : false;
type A = IsReadonlyArray<readonly number[]>; // true
type B = IsReadonlyArray<number[]>; // false
Conditional types enable "meta" checks such as immutability, tuple-ness, or structure validation.
Real-World Example: Extracting a Promise's Resolved Type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<number>>; // number
type B = UnwrapPromise<Promise<string>>; // string
type C = UnwrapPromise<number>; // number
Used in async utils, API clients, or testing frameworks.
If it's a Promise, unwrap it; otherwise, keep the type as-is.
Real-World Example: JSON Serialization Type
type JsonSafe<T> =
T extends string | number | boolean | null
? T
: T extends (infer U)[]
? JsonSafe<U>[]
: T extends object
? { [K in keyof T]: JsonSafe<T[K]> }
: never;
type User = {
id: number;
name: string;
flags: boolean[];
};
type J = JsonSafe<User>;
// {
// id: number;
// name: string;
// flags: boolean[];
// }
Shows how complex data can be recursively transformed at type level.
Conditional types allow for branching logic at each structural level.
Summary Table
Feature
Purpose
Example
Basic condition
Choose between two types
T extends U ? A : B
Distributive behavior
Map each union member
(A | B) extends X ? Y : Z
infer keyword
Extract parts of a type
infer R
Filtering unions
Keep or remove union members
string | never
Recursive conditional types
Transform deep structures
JSON-safe types
Mapped Types
Introduction
Mapped types allow you to take an existing object type and create a new one by applying the same transformation to each property.
The syntax has the form:
{ [K in SomeKeys]: SomeTransformation<T[K]> }
They are the foundation of many built-in TypeScript utilities like Partial, Readonly, Pick, Record, and Required.
You can think of mapped types as "type-level loops" that iterate over keys and build a new shape.
Basic Example
type User = {
id: number;
name: string;
};
type OptionalUser = {
[K in keyof User]?: User[K];
};
// { id?: number; name?: string }
This iterates over all keys of User and makes each property optional.
keyof User produces the union "id" | "name".
User[K] copies the original property type.
Readonly Mapped Types
type ReadonlyObject<T> = {
readonly [K in keyof T]: T[K];
};
type Product = {
price: number;
title: string;
};
type ReadonlyProduct = ReadonlyObject<Product>;
// { readonly price: number; readonly title: string }
Mapped types can add or remove modifiers like readonly or ?.
This pattern is the same one used internally by TypeScript's Readonly<T> utility.
Adding, Removing and Changing Modifiers
You can explicitly add or remove modifiers:
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type RequiredObject<T> = {
[K in keyof T]-?: T[K];
};
type OptionalObject<T> = {
[K in keyof T]?: T[K];
};
+readonly and -readonly (or simply readonly) adjust mutability.
+? and -? adjust optionality.
This allows extremely fine control over type transformations.
Remapping Keys with as
You can rename keys while mapping them using as inside mapped types.
This feature (introduced in TS 4.1) enables advanced transformations.
type PrefixKeys<T> = {
[K in keyof T as `prefix_${K}`]: T[K];
};
type Example = {
a: number;
b: string;
};
type WithPrefix = PrefixKeys<Example>;
// { prefix_a: number; prefix_b: string }
as allows computed property names, template literal types, and key filtering.
Filtering Keys
Mapped types combined with key remapping can remove certain properties from a type.
type RemoveMethods<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
type Model = {
id: number;
save(): void;
load(): void;
};
type DataOnly = RemoveMethods<Model>;
// { id: number }
Returning never for a key removes it from the resulting type.
Mapping Over Unions
Mapped types also iterate over unions of keys, not only object keys.
type EventName = "click" | "focus" | "keydown";
type EventMap = {
[E in EventName]: { type: E };
};
type M = EventMap;
// {
// click: { type: "click" },
// focus: { type: "focus" },
// keydown: { type: "keydown" }
// }
This helps construct strongly typed event maps, command tables, reducers, or configuration objects.
Mapping and Transforming Value Types
Mapped types can transform both keys and values at the same time.
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type User = {
id: number;
name: string;
};
type NullableUser = Nullable<User>;
// { id: number | null; name: string | null }
This is common for database schemas, partial updates, or form defaults.
Mapped Types Combined with Conditional Types
Mapped types often appear together with conditional types for advanced logic.
type Serialize<T> = {
[K in keyof T]: T[K] extends Function
? never
: T[K] extends object
? Serialize<T[K]>
: T[K];
};
type Model = {
id: number;
flags: { admin: boolean };
save(): void;
};
type S = Serialize<Model>;
// { id: number; flags: { admin: boolean } }
You can recursively transform each property depending on its type.
This unlocks extremely expressive type manipulation patterns.
Constructing Dictionary Types
type Dictionary<K extends string, V> = {
[P in K]: V;
};
type UserRoles = Dictionary<"admin" | "user" | "guest", boolean>;
// { admin: boolean; user: boolean; guest: boolean }
This pattern is used extensively in settings objects, configuration tables, registry maps, and permission systems.
Built-In Mapped Type Utilities
Many TS built-ins are implemented as mapped types:
type Partial<T> = {
[K in keyof T]?: T[K];
};
type Required<T> = {
[K in keyof T]-?: T[K];
};
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Record<K extends string, T> = {
[P in K]: T;
};
Understanding mapped types is essential to understanding how these built-ins work.
Summary
Concept
Explanation
Example
Key iteration
Iterate over keys with keyof
[K in keyof T]
Property copying
Reusing existing types
T[K]
Modifier changes
Add/remove readonly / ?
+readonly, -?
Key remapping
Rename or filter keys
as ...
Union mapping
Build objects from unions
[K in "a" | "b"]
The infer Keyword
infer is a special keyword used inside conditional types to extract some part of a type and assign it to a new type variable.
You can think of infer as "pattern matching for types": if the pattern fits, TypeScript extracts the needed portion.
infer only works inside conditional types of the form:
T extends SomePattern<infer X> ? Result : Fallback
Basic Example: Extracting a Function's Return Type
type ReturnTypeOf<T> =
T extends (...args: any[]) => infer R
? R
: never;
function foo() {
return { id: 1, name: "Alice" };
}
type R = ReturnTypeOf<typeof foo>;
// { id: number; name: string }
Here, infer R grabs the return type of the function.
If T does not match a function type, the result becomes never.
Extracting Function Parameter Types
type FirstArg<T> =
T extends (arg: infer A, ...rest: any[]) => any
? A
: never;
type X = FirstArg<(x: number, y: string) => void>;
// number
infer A binds the type of the first parameter.
You can extract more parameters by pattern matching the full function signature.
Extracting Tuple and Array Element Types
type ElementOf<T> =
T extends (infer U)[] ? U : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<number[]>; // number
The pattern (infer U)[] matches any array type, extracting its element type.
This is essential for building reusable collection helpers.
Extracting Tuple Element Types With Precision
type TupleFirst<T> =
T extends [infer A, ...any[]] ? A : never;
type TupleRest<T> =
T extends [any, ...infer R] ? R : never;
type T1 = TupleFirst<[1, 2, 3]>; // 1
type T2 = TupleRest<[1, 2, 3]>; // [2, 3]
You can destructure tuples using infer just like JavaScript destructures arrays.
This enables flexible tuple manipulation in type-level programming.
Extracting the Resolved Type of a Promise
type UnwrapPromise<T> =
T extends Promise<infer U> ? U : T;
type P1 = UnwrapPromise<Promise<string>>; // string
type P2 = UnwrapPromise<Promise<number>>; // number
type P3 = UnwrapPromise<number>; // number
infer U extracts the inner type of a promise.
If T is not a promise, the type remains unchanged.
Extracting Object Property Types
type ValueOf<T> =
T extends { [K: string]: infer V } ? V : never;
type Obj = { a: number; b: string; };
type V = ValueOf<Obj>;
// number | string
The pattern matches any property value, collecting all possible ones.
This is helpful for map-like objects or enums.
Extracting Constructor Instance Types
type InstanceTypeOf<T> =
T extends new (...args: any[]) => infer R
? R
: never;
class User {
constructor(public id: number) {}
}
type U = InstanceTypeOf<typeof User>;
// User
infer R captures whatever the constructor returns (the instance type).
This is similar to built-in InstanceType<T>.
Inferring Within Nested Structures
type ResponseType<T> =
T extends { data: infer D } ? D : never;
type Api = {
data: {
id: number;
name: string;
}
};
type R = ResponseType<Api>;
// { id: number; name: string }
infer works inside deeply nested object patterns.
This enables type extraction from complex APIs or validation libraries.
Combining infer with Conditional Mapped Types
type DeepUnwrapPromise<T> = {
[K in keyof T]: T[K] extends Promise<infer U>
? U
: T[K];
};
type Obj = {
name: string;
age: Promise<number>;
};
type NewObj = DeepUnwrapPromise<Obj>;
// { name: string; age: number }
Makes infer extremely powerful in generic transformations.
Real-world usage includes ORMs, serializers, schemas, and RPC frameworks.
Recursive Extraction with infer
type DeepResolve<T> =
T extends Promise<infer U>
? DeepResolve<U>
: T;
type P = DeepResolve<Promise<Promise<string>>>;
// string
infer supports recursion, enabling deeply nested type transformation.
Used in advanced async utilities or libraries like tRPC and Zod.
Using infer to Detect Arrays vs Non-Arrays
type IsArray<T> =
T extends readonly any[] ? true : false;
type A = IsArray<number[]>; // true
type B = IsArray<readonly string[]>; // true
type C = IsArray<number>; // false
You can infer tuple vs array vs non-array patterns for strict validation.
Summary Table
Pattern
Purpose
Example
infer R in function patterns
Extract return type or parameter type
(...args) => infer R
infer U in array patterns
Extract element type
(infer U)[]
infer A in tuple patterns
Extract first, last, or rest elements
[infer A, ...rest]
Nested infer
Deep extraction inside objects
{ data: infer D }
Recursive infer
Unwrap nested structures
Promise<Promise<...>>
Template Literal Types
Introduction
Template Literal Types in TypeScript allow you to construct new string types by combining:
string literal types
union types
interpolations using ${...}
They work similarly to JavaScript template strings, but at the type level.
Basic Template Literal Type
type Greeting = `Hello ${string}`;
let a: Greeting = "Hello Alice"; // OK
let b: Greeting = "Hi Bob"; // Error
The type Hello ${string} matches any string starting with "Hello ".
The ${string} placeholder means any string literal.
Using Union Types Inside Template Literals
type Direction = "up" | "down";
type Move = `move-${Direction}`;
let a: Move = "move-up"; // OK
let b: Move = "move-left"; // Error
Unions expand all combinations.
Template literal types allow building strict naming patterns.
Combining Multiple Unions
type Size = "small" | "medium" | "large";
type Color = "red" | "blue";
type Variant = `${Size}-${Color}`;
let v1: Variant = "small-red"; // OK
let v2: Variant = "large-blue"; // OK
let v3: Variant = "medium-green"; // Error
The type expands to all valid combinations automatically.
Useful for CSS-like systems, classnames, config options, etc.
Inferring Template Literal Types
function makeEvent(name: string) {
return `on-${name}` as const;
}
const e = makeEvent("click");
// type: "on-click"
Using as const helps preserve literal types instead of widening to string.
Narrowing With Template Literal Types
type ID = `id-${number}`;
function printID(id: ID) {
console.log(id);
}
printID("id-42"); // OK
printID("id-Alice"); // Error
You can require specific string patterns at compile time.
This is helpful in routing, command parsing, event names, etc.
Template Literal Types With Built-In String Manipulation Types
type Name = "alice" | "bob";
type Capitalized = Capitalize<Name>;
type Greeting = `Hello ${Capitalized}`;
let a: Greeting = "Hello Alice"; // OK
let b: Greeting = "Hello bob"; // Error
Template literal types integrate with TypeScript's utility types:
Uppercase<T>
Lowercase<T>
Capitalize<T>
Uncapitalize<T>
Building Safe API Routes With Template Literal Types
type UserID = number;
type Route = `/users/${UserID}/posts/${number}`;
let r: Route = "/users/12/posts/99"; // OK
let x: Route = "/users/A/posts/5"; // Error
Great for ensuring correctness in API path strings.
Reduces runtime errors from typos or wrong formats.
Extracting Parts Using Inference
type Event = `on-${string}`;
function handle<T extends Event>(name: T) {
const type = name.replace("on-", "");
return type;
}
handle("on-click"); // OK
handle("on-move"); // OK
handle("click"); // Error
This allows pattern-based type checking.
Useful when parsing structured strings.
Template Literals in Conditional Types
type RemovePrefix<T> = T extends `id-${infer U}` ? U : never;
type R1 = RemovePrefix<"id-123">; // "123"
type R2 = RemovePrefix<"user">; // never
You can extract substrings at the type level via infer.
Enables advanced type transformations.
Tagging and Branding Types
type BrandedID = `${number}-user-id`;
let id1: BrandedID = "123-user-id"; // OK
let id2: BrandedID = "123abc"; // Error
Template literal types help create nominal-like typing patterns.
Useful for preventing mixing of different ID types.
Summary of Template Literal Types
Feature
Description
Example
Basic construction
Create string patterns using ${}
`Hello ${string}`
Union expansion
Build combinations from unions
`move-${"left"|"right"}`
Type-safe strings
Prevent invalid formats
id-${number}
Integration with utility types
Uppercase, Lowercase, Capitalize
`Hello ${Capitalize<T>}`
Pattern inference
Extract inner substrings with infer
T extends `x-${infer U}`
API schemas
Build strict REST endpoints
`/users/${number}`
Modules in TypeScript
Introduction
A module is simply any file that contains at least one of:
export
import
Each file with exports becomes its own module and has its own scope.
Named Exports
// math.ts
export const PI = 3.14;
export function add(a: number, b: number) {
return a + b;
}
Use named exports when you want to export multiple things from a file.
Import them by name:
import { PI, add } from "./math";
Renaming Exports and Imports to Avoid Conflicts or for Clarity
// utils.ts
export function calculate() {}
import { calculate as calc } from "./utils";
calc();
function SimpleDecorator(constructor: Function) {
console.log("Decorated:", constructor.name);
}
@SimpleDecorator
class MyClass {}
@SimpleDecorator invokes the function when the class is defined.
Decorators run at definition time, not at runtime when instances are created.
Class Decorators
A class decorator receives the constructor function of the class.
function LogClass(target: Function) {
console.log(`Class: ${target.name}`);
}
@LogClass
class User {}
Output:
Class: User
Useful for logging, metadata, or modifying the constructor.
Class Decorator Factory
function WithRole(role: string) {
return function (target: Function) {
target.prototype.role = role;
};
}
@WithRole("admin")
class Account {}
const a = new Account();
console.log(a.role); // "admin"
Decorator factories allow passing parameters into decorators.
The first function receives parameters; the second receives the class.
Method Decorators
A method decorator receives:
target — the prototype
key — the method name
descriptor — the property descriptor
function LogMethod(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
}
class Calculator {
@LogMethod
add(a: number, b: number) {
return a + b;
}
}
const c = new Calculator();
c.add(2, 3);
A method decorator can wrap, modify, or replace the method.
This is the base of logging, profiling, caching, and authorization decorators.
Accessor Decorators
function Readonly(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
}
class Person {
private _name = "Alice";
@Readonly
get name() {
return this._name;
}
}
// person.name = "Bob"; // Error
Accessor decorators modify getters and setters.
They allow controlling writability or enumerability.
Property Decorators
A property decorator receives only:
target — the prototype
key — the property name
It cannot directly modify property values because properties do not have descriptors on the prototype.
function Required(target: any, key: string) {
console.log(`${key} is required`);
}
class Settings {
@Required
theme!: string;
}
Often used for validation metadata.
Parameter Decorators
function LogParam(target: any, key: string, index: number) {
console.log(`Parameter #${index} in ${key}`);
}
class Service {
greet(@LogParam name: string) {
console.log("Hello " + name);
}
}
Parameter decorators receive:
target
method name
parameter index
Useful for dependency injection systems (e.g. NestJS).
Using Decorators for Dependency Injection
function Inject(service: any) {
return function (target: any, key: string, index: number) {
console.log(`Injecting ${service.name} into ${key}`);
};
}
class Logger {}
class Controller {
constructor(@Inject(Logger) private logger: Logger) {}
}
Decorators allow building DI frameworks similar to Angular and NestJS.
Metadata Decorators
import "reflect-metadata";
function Type(type: any) {
return function (target: any, key: string) {
Reflect.defineMetadata("design:type", type, target, key);
};
}
class Box {
@Type(String)
content!: string;
}
const type = Reflect.getMetadata("design:type", new Box(), "content");
console.log(type.name); // "String"
Used heavily in ORMs and DI frameworks.
Requires emitDecoratorMetadata to be enabled.
Execution Order of Decorators
If multiple decorators are applied, evaluation happens:
Bottom to top (in code)
Then each result is applied top to bottom
function A() { return () => console.log("A"); }
function B() { return () => console.log("B"); }
@A()
@B()
class Example {}
Output:
B
A
Summary of Decorators in TypeScript
Decorator Type
Purpose
Arguments Received
Class Decorator
Modify/replace class constructor
(constructor)
Method Decorator
Modify method behavior
(target, key, descriptor)
Accessor Decorator
Control getters/setters
(target, key, descriptor)
Property Decorator
Attach metadata to fields
(target, key)
Parameter Decorator
Inject or annotate parameters
(target, key, index)
Getters and Setters in TypeScript
Introduction
TypeScript allows you to define special class methods called get and set.
These methods look like normal property access from the outside, but inside they run custom logic.
They behave like properties but work like functions.
Basic Example of Getter and Setter
class User {
private _name: string = "";
get name() {
return this._name.toUpperCase();
}
set name(value: string) {
if (value.length < 2) {
throw new Error("Name too short");
}
this._name = value;
}
}
const u = new User();
u.name = "alice"; // calls the setter
console.log(u.name); // calls the getter
Typing u.name does NOT access a field.
It triggers the getter or setter as if they were functions.
Getter and setter syntax allows you to write cleaner and more natural APIs:
u.name; // getter
u.name = x; // setter
The API feels like using normal properties.
This is why frameworks like Angular and libraries like TypeORM rely on get/set.
Getter Only (Read-Only Property)
class Circle {
constructor(private radius: number) {}
get area() {
return Math.PI * this.radius * this.radius;
}
}
const c = new Circle(10);
console.log(c.area); // computed every time
No setter means the property cannot be assigned to.
c.area = 10; causes a compile-time error.
Used for computed values or protected internal values.
Setter Only (Write-Only Property)
class Logger {
set message(value: string) {
console.log("Log:", value);
}
}
const log = new Logger();
log.message = "Hello"; // allowed
// console.log(log.message); // ERROR: getter missing
Rare in practice but used in logging or write-only channels.
Using Getters and Setters to Control Access
class Person {
private _age = 0;
get age() {
return this._age;
}
set age(value: number) {
if (value < 0 || value > 150) {
throw new Error("Invalid age");
}
this._age = value;
}
}
Useful when a property must follow rules or constraints.
Prevents invalid state from being assigned.
Getters and Setters with Types
class Point {
private _x = 0;
get x(): number {
return this._x;
}
set x(value: number) {
this._x = value;
}
}
The getter declares a return type.
The setter declares a parameter type.
They must match or be compatible.
How They Work Internally (Property Descriptor)
TypeScript compiles getters and setters to JavaScript using Object.defineProperty.
Object.defineProperty(Person.prototype, "age", {
get: function () { ... },
set: function (value) { ... },
enumerable: true,
configurable: true
});
This is the true magic behind get and set.
The property is stored on the prototype as an accessor, not as a normal field.
Reading it calls the getter function; writing it calls the setter function.
Getters and Setters with Private Fields
class Account {
#balance = 0;
get balance() {
return this.#balance;
}
set balance(value: number) {
if (value < 0) throw new Error("Negative balance not allowed");
this.#balance = value;
}
}
#balance is a true private field at runtime (ES private field).
Getters and setters act as a controlled interface to private data.
Getters and Setters with Computation
class Temperature {
constructor(private celsius: number) {}
get fahrenheit() {
return this.celsius * 1.8 + 32;
}
set fahrenheit(value: number) {
this.celsius = (value - 32) / 1.8;
}
}
The getter computes Fahrenheit from Celsius.
The setter reverses the calculation.
This creates virtual fields that appear real but are computed.
Combining Getters, Setters, and Decorators
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const originalSet = descriptor.set!;
descriptor.set = function (value: any) {
console.log(`Assigning ${key} =`, value);
originalSet.call(this, value);
};
}
class Product {
private _price = 0;
@Log
set price(value: number) {
this._price = value;
}
get price() {
return this._price;
}
}
Setters are widely used together with decorators to implement validation, logging, or metadata.