Functions & Types
Functions are the building blocks of any application. TypeScript makes functions safer by typing parameters, return values, and function signatures themselves.
1. Function Type Annotations
Parameter and Return Types
// Annotate parameters and return type
function add(a: number, b: number): number {
return a + b;
}
// Arrow function
const multiply = (a: number, b: number): number => a * b;
// Return type is often inferred
const divide = (a: number, b: number) => a / b; // Returns number
void and never
// void - function doesn't return anything
function log(message: string): void {
console.log(message);
}
// never - function never returns (throws or infinite loop)
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
2. Optional and Default Parameters
Optional Parameters
function greet(name: string, greeting?: string): string {
return `${greeting || 'Hello'}, ${name}!`;
}
greet('Alice'); // 'Hello, Alice!'
greet('Alice', 'Hi'); // 'Hi, Alice!'
Default Parameters
function greet(name: string, greeting: string = 'Hello'): string {
return `${greeting}, ${name}!`;
}
greet('Alice'); // 'Hello, Alice!'
greet('Alice', 'Hi'); // 'Hi, Alice!'
Optional vs Default: Optional parameters can be undefined. Default parameters have a fallback value. Default parameters are usually preferred.
Rest Parameters
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15
3. Function Type Expressions
Define the type of a function itself.
// Type alias for a function
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
// As a parameter
function calculate(a: number, b: number, operation: MathOperation): number {
return operation(a, b);
}
calculate(10, 5, add); // 15
calculate(10, 5, subtract); // 5
Call Signatures
// Object type with call signature
type DescribableFunction = {
description: string;
(x: number): number;
};
function double(x: number): number {
return x * 2;
}
double.description = 'Doubles the input';
const fn: DescribableFunction = double;
Construct Signatures
// For classes/constructors
type UserConstructor = {
new (name: string): User;
};
class User {
constructor(public name: string) {}
}
function createUser(ctor: UserConstructor, name: string): User {
return new ctor(name);
}
const user = createUser(User, 'Alice');
4. Function Overloads
Define multiple function signatures for different parameter combinations.
// Overload signatures
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;
// Implementation signature
function format(value: string | number | Date): string {
if (typeof value === 'string') {
return value.toUpperCase();
} else if (typeof value === 'number') {
return value.toFixed(2);
} else {
return value.toISOString();
}
}
format('hello'); // 'HELLO'
format(3.14159); // '3.14'
format(new Date()); // '2024-01-01T00:00:00.000Z'
More Complex Overloads
// Return different types based on input
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: 'span'): HTMLSpanElement;
function createElement(tag: 'input'): HTMLInputElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
const div = createElement('div'); // HTMLDivElement
const span = createElement('span'); // HTMLSpanElement
const input = createElement('input'); // HTMLInputElement
const custom = createElement('custom'); // HTMLElement
Overloads Rule: The implementation signature must be compatible with all overload signatures but is not visible to callers.
5. Generic Functions
Write functions that work with any type while maintaining type safety.
Basic Generics
// Without generics
function identity(value: any): any {
return value;
}
// With generics - type is preserved!
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // number
const str = identity('hello'); // string
const obj = identity({ x: 1 }); // { x: number }
Multiple Type Parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p1 = pair('hello', 42); // [string, number]
const p2 = pair(true, { name: 'Alice' }); // [boolean, { name: string }]
Generic Constraints
// Constrain T to have a length property
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
getLength('hello'); // 5
getLength([1, 2, 3]); // 3
getLength({ length: 10 }); // 10
// getLength(123); // ❌ Error: number doesn't have length
keyof Constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 25 };
getProperty(user, 'name'); // string
getProperty(user, 'age'); // number
// getProperty(user, 'email'); // ❌ Error: 'email' is not a key of user
6. Type Guards
Functions that narrow types at runtime.
typeof Guards
function processValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toString();
}
instanceof Guards
class Dog {
bark() { console.log('Woof!'); }
}
class Cat {
meow() { console.log('Meow!'); }
}
function makeSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
Custom Type Guards (Type Predicates)
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
// Custom type guard with 'is' keyword
function isFish(animal: Fish | Bird): animal is Fish {
return (animal as Fish).swim !== undefined;
}
function move(animal: Fish | Bird): void {
if (isFish(animal)) {
animal.swim(); // TypeScript knows it's Fish
} else {
animal.fly(); // TypeScript knows it's Bird
}
}
Discriminated Unions
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
}
}
const circle: Shape = { kind: 'circle', radius: 5 };
getArea(circle); // 78.54
7. Assertion Functions
Functions that throw if a condition is false, narrowing the type.
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value must be a string');
}
}
function processInput(input: unknown): string {
assertIsString(input);
// TypeScript knows input is string after assertion
return input.toUpperCase();
}
Non-null Assertion
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('Value must be defined');
}
}
function getUser(id: string): User | null {
// ... fetch user
return null;
}
const user = getUser('123');
assertDefined(user);
// TypeScript knows user is User (not null) after this point
console.log(user.name);
8. this Parameter
Explicitly type the this context.
interface User {
name: string;
greet(this: User): string;
}
const user: User = {
name: 'Alice',
greet() {
return `Hello, ${this.name}!`;
}
};
user.greet(); // 'Hello, Alice!'
// This would error because 'this' context is wrong
const greet = user.greet;
// greet(); // ❌ Error: 'this' context of type 'void' is not assignable
This Parameter in Classes
class Counter {
count = 0;
// Arrow function preserves 'this'
increment = () => {
this.count++;
};
// Regular method with explicit this type
decrement(this: Counter): void {
this.count--;
}
}
9. Callback Types
Type callbacks precisely.
// Simple callback
type Callback = (result: string) => void;
function fetchData(callback: Callback): void {
setTimeout(() => {
callback('Data loaded');
}, 1000);
}
// Callback with error handling
type NodeCallback<T> = (error: Error | null, result: T | null) => void;
function readFile(path: string, callback: NodeCallback<string>): void {
// ...
}
// Event handler
type EventHandler<E extends Event> = (event: E) => void;
const handleClick: EventHandler<MouseEvent> = (event) => {
console.log(event.clientX, event.clientY);
};
10. Practical Examples
API Function Types
interface User {
id: number;
name: string;
email: string;
}
type ApiResponse<T> = {
data: T;
status: number;
message: string;
};
async function fetchUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
async function fetchUsers(): Promise<ApiResponse<User[]>> {
const response = await fetch('/api/users');
return response.json();
}
Event Emitter Pattern
type EventMap = {
login: { userId: string };
logout: { userId: string };
error: { message: string; code: number };
};
type EventCallback<T> = (data: T) => void;
class EventEmitter<T extends Record<string, any>> {
private listeners = new Map<keyof T, Set<EventCallback<any>>>();
on<K extends keyof T>(event: K, callback: EventCallback<T[K]>): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
}
emit<K extends keyof T>(event: K, data: T[K]): void {
this.listeners.get(event)?.forEach((callback) => callback(data));
}
}
const emitter = new EventEmitter<EventMap>();
emitter.on('login', (data) => {
console.log(`User ${data.userId} logged in`);
});
emitter.emit('login', { userId: '123' }); // Type-safe!
Summary
| Concept | Example |
|---|
| Parameter Types | function add(a: number, b: number) |
| Return Types | function add(a, b): number |
| Optional Params | function greet(name: string, greeting?: string) |
| Default Params | function greet(name: string, greeting = 'Hello') |
| Rest Params | function sum(...nums: number[]) |
| Function Types | type Fn = (x: number) => number |
| Overloads | Multiple signatures + implementation |
| Generics | function identity<T>(value: T): T |
| Type Guards | function isFish(x): x is Fish |
| Assertions | function assert(x): asserts x is string |
Next, we’ll explore objects and interfaces in depth!