TypeScript Blitz - Day 3: Functions & Advanced Types
Master TypeScript function typing - learn overloads, callbacks, rest parameters, this typing, and utility types like ReturnType and Parameters
Goal
After this lesson you’ll type functions with overloads and handle callback hell like a pro.
Theory Part (20 min)
1. Function Signatures - Basic Typing
// Basic function with typed parameters and return
function add(a: number, b: number): number {
return a + b;
}
// Arrow function
const multiply = (a: number, b: number): number => {
return a * b;
};
// Void return type - function doesn't return anything
function log(message: string): void {
console.log(message);
// no return statement
}
// Function type as variable
let operation: (x: number, y: number) => number;
operation = add; // ✅ OK
operation = multiply; // ✅ OK
// operation = log; // ❌ Error - wrong signature
// Type alias for function
type MathOperation = (x: number, y: number) => number;
const divide: MathOperation = (a, b) => {
return a / b;
// Parameters a, b are inferred as number from type
};
2. Optional and Default Parameters
// Optional parameters with ?
function greet(name: string, greeting?: string): string {
if (greeting) {
return `${greeting}, ${name}!`;
}
return `Hello, ${name}!`;
}
greet("John"); // ✅ "Hello, John!"
greet("John", "Hi"); // ✅ "Hi, John!"
// greet(); // ❌ Error - name is required
// Default parameters
function createUser(name: string, age: number = 18): User {
return { name, age };
}
createUser("John"); // age = 18
createUser("Anna", 25); // age = 25
// Optional must come after required
function build(required: string, optional?: number): void {
// ✅ OK
}
// function build2(optional?: number, required: string): void {
// // ❌ Error - optional before required
// }
// Default parameters can be anywhere
function build3(x: number = 0, y: number): number {
return x + y;
}
build3(undefined, 5); // x = 0, y = 5
3. Rest Parameters - Variable Number of Arguments
// Rest parameters - array of values
function sum(...numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15
sum(); // 0
// Rest parameters with other parameters
function buildMessage(prefix: string, ...parts: string[]): string {
return prefix + " " + parts.join(" ");
}
buildMessage("Hello", "world", "from", "TypeScript");
// "Hello world from TypeScript"
// Typing rest parameters with tuples
function logData(message: string, ...data: [number, boolean]): void {
console.log(message, data[0], data[1]);
}
logData("Info", 42, true); // ✅ OK
// logData("Info", 42); // ❌ Error - missing boolean
// logData("Info", 42, true, "extra"); // ❌ Error - too many args
4. Function Overloads - Multiple Signatures
// Overload signatures
function process(input: string): string[];
function process(input: number): number[];
function process(input: boolean): boolean;
// Implementation signature (must be compatible with all overloads)
function process(input: string | number | boolean): any {
if (typeof input === "string") {
return input.split("");
} else if (typeof input === "number") {
return [input, input * 2, input * 3];
} else {
return input;
}
}
const result1 = process("hello"); // string[]
const result2 = process(5); // number[]
const result3 = process(true); // boolean
// Real-world example: API fetch with different return types
function fetchData(id: number): Promise<User>;
function fetchData(email: string): Promise<User>;
function fetchData(filter: { active: boolean }): Promise<User[]>;
function fetchData(
param: number | string | { active: boolean }
): Promise<User | User[]> {
if (typeof param === "number") {
return fetch(`/api/users/${param}`).then(r => r.json());
} else if (typeof param === "string") {
return fetch(`/api/users?email=${param}`).then(r => r.json());
} else {
return fetch(`/api/users?active=${param.active}`).then(r => r.json());
}
}
// TypeScript knows exact return type based on argument
const user1 = await fetchData(123); // User
const user2 = await fetchData("john@email"); // User
const users = await fetchData({ active: true }); // User[]
Important: Overload signatures are for TypeScript only - at runtime there’s only one implementation.
5. this Typing - Context-Aware Functions
// Typing 'this' parameter (first parameter, not counted in actual params)
interface Button {
label: string;
onClick(this: Button, event: Event): void;
}
const button: Button = {
label: "Click me",
onClick(this: Button, event: Event) {
console.log(this.label); // ✅ this is Button
// 'this' is guaranteed to be Button type
}
};
// Arrow functions don't have 'this' binding
interface Handler {
value: number;
handle: (event: Event) => void;
}
const handler: Handler = {
value: 42,
handle: (event: Event) => {
// 'this' refers to outer scope, not handler object
// console.log(this.value); // Error in strict mode
}
};
// ThisType utility
interface State {
name: string;
age: number;
}
interface Methods {
setName(name: string): void;
setAge(age: number): void;
}
type Store = State & ThisType<State & Methods>;
const store: Store = {
name: "John",
age: 30,
setName(name: string) {
this.name = name; // 'this' has access to State & Methods
},
setAge(age: number) {
this.age = age;
}
};
6. Callback Functions - Type-Safe Callbacks
// Simple callback
function processData(
data: string[],
callback: (item: string) => void
): void {
data.forEach(callback);
}
processData(["a", "b", "c"], (item) => {
console.log(item.toUpperCase()); // item is string
});
// Callback with return value
function map<T, U>(
array: T[],
callback: (item: T, index: number) => U
): U[] {
return array.map(callback);
}
const numbers = [1, 2, 3];
const doubled = map(numbers, (n) => n * 2); // number[]
const strings = map(numbers, (n) => n.toString()); // string[]
// Error callbacks (Node.js style)
function readFile(
path: string,
callback: (error: Error | null, data: string | null) => void
): void {
// Simulated async operation
setTimeout(() => {
if (path.endsWith(".txt")) {
callback(null, "file content");
} else {
callback(new Error("Invalid file"), null);
}
}, 100);
}
readFile("test.txt", (error, data) => {
if (error) {
console.error(error.message); // error: Error
} else {
console.log(data?.toUpperCase()); // data: string | null
}
});
// Promise-based callbacks
function asyncProcess<T>(
data: T,
onSuccess: (result: T) => void,
onError: (error: Error) => void
): void {
try {
// Process data
onSuccess(data);
} catch (err) {
onError(err as Error);
}
}
7. Utility Types for Functions
// ReturnType<T> - extract return type
function getUser() {
return { id: 1, name: "John", email: "john@example.com" };
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string; }
// Parameters<T> - extract parameter types as tuple
function createPost(title: string, content: string, published: boolean) {
return { title, content, published };
}
type CreatePostParams = Parameters<typeof createPost>;
// [string, string, boolean]
// Can destructure
type FirstParam = CreatePostParams[0]; // string
type SecondParam = CreatePostParams[1]; // string
// Practical use - wrapper functions
function loggedCreatePost(...args: Parameters<typeof createPost>) {
console.log("Creating post with:", args);
return createPost(...args);
}
// ConstructorParameters<T> - for class constructors
class User {
constructor(public name: string, public age: number) {}
}
type UserParams = ConstructorParameters<typeof User>;
// [string, number]
function createUser(...args: UserParams): User {
return new User(...args);
}
Practice Part (35 min)
Exercise 1: Type-Safe API Client with Overloads (15 min)
Create a flexible API client that can handle different request types.
// TODO: Create function overloads for different HTTP methods
// GET request - returns Promise<T>
// function api(method: "GET", url: string): Promise<unknown>;
// POST request - takes body, returns Promise<T>
// function api(method: "POST", url: string, body: object): Promise<unknown>;
// DELETE request - returns Promise<void>
// function api(method: "DELETE", url: string): Promise<void>;
// Implementation
// function api(
// method: string,
// url: string,
// body?: object
// ): Promise<any> {
// // TODO: Implement
// }
// Test types
interface User {
id: number;
name: string;
email: string;
}
// These should work with correct return types:
// const user = await api("GET", "/users/1"); // Promise<unknown>
// const created = await api("POST", "/users", { // Promise<unknown>
// name: "John",
// email: "john@example.com"
// });
// await api("DELETE", "/users/1"); // Promise<void>
// Bonus: Make it generic to specify return type
// const user = await api<User>("GET", "/users/1"); // Promise<User>
Solution:
Click to see solution
// Overload signatures
function api<T = unknown>(method: "GET", url: string): Promise<T>;
function api<T = unknown>(method: "POST", url: string, body: object): Promise<T>;
function api(method: "DELETE", url: string): Promise<void>;
// Implementation signature
function api<T = unknown>(
method: "GET" | "POST" | "DELETE",
url: string,
body?: object
): Promise<T | void> {
const options: RequestInit = {
method,
headers: {
"Content-Type": "application/json",
},
};
if (body) {
options.body = JSON.stringify(body);
}
return fetch(url, options).then(response => {
if (method === "DELETE") {
return;
}
return response.json();
});
}
// Usage with type inference
interface User {
id: number;
name: string;
email: string;
}
async function example() {
// GET - returns Promise<User>
const user = await api<User>("GET", "/users/1");
console.log(user.name); // ✅ user: User
// POST - returns Promise<User>
const created = await api<User>("POST", "/users", {
name: "John",
email: "john@example.com"
});
console.log(created.id); // ✅ created: User
// DELETE - returns Promise<void>
await api("DELETE", "/users/1");
// No return value
}
Exercise 2: Event Emitter with Type-Safe Callbacks (12 min)
Build a type-safe event emitter.
// TODO: Create EventEmitter class with type-safe events
// Events map - defines event names and their payload types
interface Events {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"data:update": { id: number; data: unknown };
"error": { message: string; code: number };
}
class EventEmitter {
// TODO: Store listeners for each event
// private listeners: ???
// TODO: on<K>(event: K, callback: (payload: Events[K]) => void)
// Subscribe to event with type-safe payload
// TODO: emit<K>(event: K, payload: Events[K])
// Emit event with type-safe payload
// TODO: off<K>(event: K, callback: (payload: Events[K]) => void)
// Unsubscribe from event
}
// Usage example:
// const emitter = new EventEmitter();
// emitter.on("user:login", (data) => {
// console.log(data.userId, data.timestamp); // ✅ data is typed!
// });
// emitter.emit("user:login", {
// userId: "123",
// timestamp: Date.now()
// }); // ✅ OK
// emitter.emit("user:login", {
// userId: "123"
// // ❌ Error - missing timestamp
// });
// emitter.on("error", (data) => {
// console.log(data.message, data.code); // ✅ data is typed!
// });
Solution:
Click to see solution
interface Events {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"data:update": { id: number; data: unknown };
"error": { message: string; code: number };
}
type EventCallback<T> = (payload: T) => void;
class EventEmitter {
private listeners: {
[K in keyof Events]?: EventCallback<Events[K]>[];
} = {};
on<K extends keyof Events>(
event: K,
callback: EventCallback<Events[K]>
): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
const callbacks = this.listeners[event];
if (callbacks) {
callbacks.forEach(callback => callback(payload));
}
}
off<K extends keyof Events>(
event: K,
callback: EventCallback<Events[K]>
): void {
const callbacks = this.listeners[event];
if (callbacks) {
this.listeners[event] = callbacks.filter(cb => cb !== callback) as any;
}
}
}
// Usage
const emitter = new EventEmitter();
emitter.on("user:login", (data) => {
console.log(`User ${data.userId} logged in at ${data.timestamp}`);
// data is { userId: string; timestamp: number }
});
emitter.on("error", (data) => {
console.error(`Error ${data.code}: ${data.message}`);
// data is { message: string; code: number }
});
emitter.emit("user:login", {
userId: "user-123",
timestamp: Date.now()
});
emitter.emit("error", {
message: "Something went wrong",
code: 500
});
Exercise 3: Array Utility Functions with Callbacks (8 min)
Implement array utilities with type-safe callbacks.
// TODO: Implement these utility functions
// 1. filter - filters array based on predicate
function filter<T>(
array: T[],
predicate: (item: T, index: number) => boolean
): T[] {
// TODO: Implement
}
// 2. groupBy - groups array items by key
function groupBy<T, K extends string | number>(
array: T[],
keyFn: (item: T) => K
): Record<K, T[]> {
// TODO: Implement
}
// 3. reduce - reduces array to single value
function reduce<T, U>(
array: T[],
reducer: (accumulator: U, current: T, index: number) => U,
initialValue: U
): U {
// TODO: Implement
}
// Tests:
interface Product {
id: number;
name: string;
category: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: "Laptop", category: "electronics", price: 999 },
{ id: 2, name: "Phone", category: "electronics", price: 699 },
{ id: 3, name: "Desk", category: "furniture", price: 299 },
];
// const expensive = filter(products, p => p.price > 500);
// console.log(expensive); // Laptop, Phone
// const byCategory = groupBy(products, p => p.category);
// console.log(byCategory); // { electronics: [...], furniture: [...] }
// const totalPrice = reduce(products, (sum, p) => sum + p.price, 0);
// console.log(totalPrice); // 1997
Solution:
Click to see solution
function filter<T>(
array: T[],
predicate: (item: T, index: number) => boolean
): T[] {
const result: T[] = [];
for (let i = 0; i < array.length; i++) {
if (predicate(array[i], i)) {
result.push(array[i]);
}
}
return result;
}
function groupBy<T, K extends string | number>(
array: T[],
keyFn: (item: T) => K
): Record<K, T[]> {
const result = {} as Record<K, T[]>;
for (const item of array) {
const key = keyFn(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
}
return result;
}
function reduce<T, U>(
array: T[],
reducer: (accumulator: U, current: T, index: number) => U,
initialValue: U
): U {
let accumulator = initialValue;
for (let i = 0; i < array.length; i++) {
accumulator = reducer(accumulator, array[i], i);
}
return accumulator;
}
// Tests
interface Product {
id: number;
name: string;
category: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: "Laptop", category: "electronics", price: 999 },
{ id: 2, name: "Phone", category: "electronics", price: 699 },
{ id: 3, name: "Desk", category: "furniture", price: 299 },
];
const expensive = filter(products, p => p.price > 500);
console.log(expensive); // [Laptop, Phone]
const byCategory = groupBy(products, p => p.category);
console.log(byCategory);
// { electronics: [Laptop, Phone], furniture: [Desk] }
const totalPrice = reduce(products, (sum, p) => sum + p.price, 0);
console.log(totalPrice); // 1997
// Type-safe - these would error:
// filter(products, p => p.nonExistent); // ❌ Error
// groupBy(products, p => p.price > 500); // ❌ Error - returns boolean, not string/number
Quick Review - Key Concepts
1. How do function overloads work?
Answer
Function overloads allow defining multiple function signatures for the same function, enabling different parameter and return types based on how it’s called.
Structure:
// Overload signatures (visible to TypeScript)
function process(input: string): string[];
function process(input: number): number[];
// Implementation signature (must be compatible with all overloads)
function process(input: string | number): string[] | number[] {
if (typeof input === "string") {
return input.split("");
} else {
return [input, input * 2];
}
}
How it works:
- TypeScript checks which overload signature matches the call
- Uses that signature’s return type
- At runtime, only the implementation exists
When I use it:
- Same function name but different input types require different return types
- API methods with different parameter combinations
- Better than union types when return type depends on input type
Example:
function fetchData(id: number): Promise<User>;
function fetchData(email: string): Promise<User>;
function fetchData(filter: Filter): Promise<User[]>;
TypeScript knows fetchData(123) returns Promise<User>, not Promise<User | User[]>.
2. What’s the difference between ReturnType and Parameters utility types?
Answer
ReturnType
Parameters
Examples:
function getUser(id: number): { name: string; age: number } {
return { name: "John", age: 30 };
}
// ReturnType - get what function returns
type UserData = ReturnType<typeof getUser>;
// { name: string; age: number }
// Parameters - get function parameters as tuple
type GetUserParams = Parameters<typeof getUser>;
// [number]
function createPost(title: string, content: string, published: boolean) {
return { title, content, published };
}
type CreatePostParams = Parameters<typeof createPost>;
// [string, string, boolean]
// Can access individual parameters
type FirstParam = CreatePostParams[0]; // string
When I use them:
- ReturnType - when I need the return type but don’t want to define it separately
- Parameters - for wrapper functions that forward arguments
Practical use:
// Wrapper function
function logged<T extends (...args: any[]) => any>(
fn: T,
...args: Parameters<T>
): ReturnType<T> {
console.log("Calling with:", args);
return fn(...args);
}
3. How to type ‘this’ in functions?
Answer
‘this’ parameter is a special first parameter in TypeScript (not counted as real parameter) that types the context.
Syntax:
interface Button {
label: string;
onClick(this: Button, event: Event): void;
}
const button: Button = {
label: "Click me",
onClick(this: Button, event: Event) {
console.log(this.label); // 'this' is guaranteed to be Button
}
};
Key points:
- First parameter named ‘this’ - not counted in actual parameters
- TypeScript-only - removed at runtime
- Arrow functions don’t have ‘this’ binding - can’t use this typing
Example:
class Component {
name = "MyComponent";
// Regular function - can type 'this'
handleClick(this: Component, event: Event) {
console.log(this.name); // ✅ this: Component
}
// Arrow function - 'this' from outer scope
handleHover = (event: Event) => {
console.log(this.name); // 'this' is Component instance
};
}
When I use it:
- Event handlers where ‘this’ context matters
- Callback functions that rely on specific context
- Method borrowing scenarios
ThisType utility:
type Methods = {
method1(): void;
method2(): void;
} & ThisType<{ value: number }>;
// Inside methods, 'this' has { value: number }
4. What are rest parameters and how to type them?
Answer
Rest parameters collect remaining arguments into an array using ... syntax.
Basic typing:
function sum(...numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15
With other parameters:
function log(level: string, ...messages: string[]): void {
console.log(`[${level}]`, ...messages);
}
log("INFO", "User", "logged", "in");
Tuple typing for fixed rest parameters:
function process(
name: string,
...args: [number, boolean]
): void {
const [id, active] = args;
// id: number, active: boolean
}
process("test", 123, true); // ✅ OK
// process("test", 123); // ❌ Error - missing boolean
With generics:
function call<T extends any[]>(
fn: (...args: T) => void,
...args: T
): void {
fn(...args);
}
Practical use:
// Wrapper that forwards all arguments
function logged<T extends any[], R>(
fn: (...args: T) => R,
...args: T
): R {
console.log("Args:", args);
return fn(...args);
}
Rest parameters must be last in parameter list.
5. How to type callback functions properly?
Answer
Callback typing depends on the callback’s purpose and signature.
Basic callback:
function forEach<T>(
array: T[],
callback: (item: T, index: number) => void
): void {
for (let i = 0; i < array.length; i++) {
callback(array[i], i);
}
}
forEach([1, 2, 3], (num, idx) => {
console.log(num, idx); // num: number, idx: number
});
Callback with return value:
function map<T, U>(
array: T[],
callback: (item: T) => U
): U[] {
return array.map(callback);
}
const strings = map([1, 2, 3], n => n.toString()); // string[]
Error-first callbacks (Node.js style):
function readFile(
path: string,
callback: (error: Error | null, data: string | null) => void
): void {
// ...
}
readFile("file.txt", (err, data) => {
if (err) {
console.error(err); // err: Error
} else {
console.log(data); // data: string | null
}
});
Optional callback:
function process(
data: string,
onComplete?: (result: string) => void
): void {
const result = data.toUpperCase();
onComplete?.(result); // Optional chaining
}
Type alias for complex callbacks:
type ErrorHandler = (error: Error) => void;
type SuccessHandler<T> = (data: T) => void;
function asyncOperation<T>(
onSuccess: SuccessHandler<T>,
onError: ErrorHandler
): void {
// ...
}
Best practices:
- Use descriptive parameter names
- Consider optional callbacks with
? - Use generics for reusable callback types
- Type both parameters and return value
Checklist - What You Should Know After Day 3
- Type functions with parameters and return types
- Use optional and default parameters
- Work with rest parameters
- Create function overloads
- Type ‘this’ parameter when needed
- Type callback functions properly
- Use ReturnType and Parameters utility types
- Handle async callbacks and promises
Quick Reference Card
// Basic function typing
function add(a: number, b: number): number {
return a + b;
}
// Optional & default parameters
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}`;
}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// Function overloads
function process(input: string): string[];
function process(input: number): number[];
function process(input: string | number): any {
// implementation
}
// Callback typing
function map<T, U>(
array: T[],
callback: (item: T) => U
): U[] {
return array.map(callback);
}
// Utility types
type Params = Parameters<typeof myFunction>;
type Return = ReturnType<typeof myFunction>;
Tomorrow: Generics - the foundation for reusable, type-safe code