Master TypeScript function signatures, parameters, return types, and advanced function patterns. Learn function overloading, generic functions, and how to create type-safe, reusable function utilities.
Functions are the building blocks of any application, and TypeScript's function typing system makes them incredibly powerful and safe. In this lesson, you'll learn how to add type annotations to functions, work with different parameter patterns, and create sophisticated function utilities that prevent bugs and improve code maintainability.
TypeScript's function typing goes far beyond just adding types - it enables function overloading, generic functions, and advanced patterns that are impossible in plain JavaScript. You'll discover how to create functions that are not only type-safe but also self-documenting and IDE-friendly.
By the end of this lesson, you'll be able to create robust, type-safe functions that scale with your application and provide excellent developer experience through IntelliSense and compile-time error checking.
TypeScript function typing starts with parameter and return type annotations. This foundational concept prevents countless runtime errors and makes your functions self-documenting.
Basic Function Type Annotations:
1// Function with typed parameters and return type
2function greet(name: string): string {
3 return `Hello, ${name}!`;
4}
5
6// Arrow function with types
7const add = (a: number, b: number): number => {
8 return a + b;
9};
10
11// Function with no return value (void)
12function logMessage(message: string): void {
13 console.log(message);
14}
15
16// Function that never returns (never type)
17function throwError(message: string): never {
18 throw new Error(message);
19}Type Inference vs Explicit Types:
1// TypeScript can infer return type
2function multiply(a: number, b: number) {
3 return a * b; // Inferred as number
4}
5
6// But explicit types are often better for clarity
7function divide(a: number, b: number): number {
8 if (b === 0) {
9 throw new Error("Division by zero");
10 }
11 return a / b;
12}Complex Return Types:
1// Function returning object type
2function createUser(name: string, age: number): { name: string; age: number; id: number } {
3 return {
4 name,
5 age,
6 id: Math.floor(Math.random() * 1000)
7 };
8}
9
10// Function returning array type
11function getNumbers(count: number): number[] {
12 return Array.from({ length: count }, (_, i) => i + 1);
13}
14
15// Function returning Promise
16async function fetchUserData(id: number): Promise<{ name: string; email: string }> {
17 const response = await fetch(`/api/users/${id}`);
18 return response.json();
19}Function Type Variables:
1// Function type as variable type
2let calculator: (a: number, b: number) => number;
3
4calculator = (x, y) => x + y;
5calculator = (x, y) => x * y;
6// calculator = (x, y) => x.toString(); // Error: Type 'string' is not assignable to type 'number'
7
8// Function type in interface
9interface MathOperations {
10 add: (a: number, b: number) => number;
11 subtract: (a: number, b: number) => number;
12 multiply: (a: number, b: number) => number;
13}TypeScript provides powerful parameter patterns that make functions flexible while maintaining type safety.
Optional Parameters:
1// Optional parameters with ?
2function formatName(firstName: string, lastName?: string): string {
3 if (lastName) {
4 return `${firstName} ${lastName}`;
5 }
6 return firstName;
7}
8
9formatName("John"); // "John"
10formatName("John", "Doe"); // "John Doe"
11// formatName(); // Error: Expected at least 1 arguments
12
13// Optional parameters must come after required ones
14function createUser(name: string, email?: string, age?: number): object {
15 return { name, email: email || "not provided", age: age || 0 };
16}Default Parameters:
1// Default parameters with type inference
2function greetUser(name: string, greeting: string = "Hello"): string {
3 return `${greeting}, ${name}!`;
4}
5
6// Explicit types with defaults
7function createArray<T>(length: number = 10, fillValue: T = null as T): T[] {
8 return Array(length).fill(fillValue);
9}
10
11// Default parameters with complex types
12interface ConnectionConfig {
13 host: string;
14 port: number;
15 ssl: boolean;
16}
17
18function connect(config: ConnectionConfig = { host: "localhost", port: 3000, ssl: false }): void {
19 console.log(`Connecting to ${config.host}:${config.port}`);
20}Rest Parameters:
1// Rest parameters with arrays
2function sum(...numbers: number[]): number {
3 return numbers.reduce((total, num) => total + num, 0);
4}
5
6sum(1, 2, 3, 4, 5); // 15
7
8// Rest parameters with mixed types
9function logMessages(level: string, ...messages: string[]): void {
10 messages.forEach(message => {
11 console.log(`[${level}] ${message}`);
12 });
13}
14
15logMessages("INFO", "Application started", "Database connected");
16
17// Rest parameters with tuple types
18function processData(operation: string, ...data: [number, string, boolean]): void {
19 const [id, name, active] = data;
20 console.log(`${operation}: ${id}, ${name}, ${active}`);
21}
22
23processData("UPDATE", 123, "John", true);Combining Parameter Patterns:
1// Required, optional, default, and rest parameters together
2function complexFunction(
3 required: string,
4 optional?: number,
5 defaultParam: boolean = true,
6 ...rest: string[]
7): { required: string; optional?: number; defaultParam: boolean; rest: string[] } {
8 return {
9 required,
10 optional,
11 defaultParam,
12 rest
13 };
14}
15
16complexFunction("test");
17complexFunction("test", 42);
18complexFunction("test", 42, false, "extra1", "extra2");Destructuring Parameters:
1// Object destructuring with types
2function createEmployee({ name, age, department }: { name: string; age: number; department: string }): object {
3 return { name, age, department, id: Math.random() };
4}
5
6// Array destructuring with types
7function processCoordinates([x, y, z]: [number, number, number]): number {
8 return Math.sqrt(x * x + y * y + z * z);
9}
10
11// Destructuring with defaults
12function configureApp({
13 host = "localhost",
14 port = 3000,
15 ssl = false
16}: { host?: string; port?: number; ssl?: boolean } = {}): void {
17 console.log(`Server: ${host}:${port}, SSL: ${ssl}`);
18}Function overloading and generics enable creating flexible, reusable functions that work with multiple types while maintaining type safety.
Function Overloading:
1// Multiple function signatures for different use cases
2function format(value: string): string;
3function format(value: number): string;
4function format(value: Date): string;
5function format(value: string | number | Date): string {
6 if (typeof value === "string") {
7 return value.toUpperCase();
8 }
9 if (typeof value === "number") {
10 return value.toFixed(2);
11 }
12 if (value instanceof Date) {
13 return value.toISOString();
14 }
15 return String(value);
16}
17
18// Usage with type safety
19const stringResult = format("hello"); // Type: string
20const numberResult = format(123.456); // Type: string
21const dateResult = format(new Date()); // Type: stringAdvanced Function Overloading:
1// Overloading with different return types
2function getElementById(id: string): HTMLElement | null;
3function getElementById<T extends HTMLElement>(id: string, type: new() => T): T | null;
4function getElementById<T extends HTMLElement>(id: string, type?: new() => T): HTMLElement | T | null {
5 const element = document.getElementById(id);
6 if (type && element instanceof type) {
7 return element as T;
8 }
9 return element;
10}
11
12// Usage
13const genericElement = getElementById("myId"); // HTMLElement | null
14const buttonElement = getElementById("myButton", HTMLButtonElement); // HTMLButtonElement | nullGeneric Functions:
1// Basic generic function
2function identity<T>(arg: T): T {
3 return arg;
4}
5
6const stringIdentity = identity<string>("hello"); // Type: string
7const numberIdentity = identity<number>(42); // Type: number
8const autoInferred = identity("world"); // Type inferred as string
9
10// Generic functions with constraints
11function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
12 return obj[key];
13}
14
15const person = { name: "John", age: 30, email: "[email protected]" };
16const name = getProperty(person, "name"); // Type: string
17const age = getProperty(person, "age"); // Type: number
18// const invalid = getProperty(person, "invalid"); // Error: Argument of type '"invalid"' is not assignableMultiple Generic Parameters:
1// Function with multiple generic types
2function combine<T, U>(first: T, second: U): { first: T; second: U } {
3 return { first, second };
4}
5
6const combined = combine("hello", 42); // Type: { first: string; second: number }
7
8// Generic function with conditional logic
9function process<T extends string | number>(
10 value: T,
11 processor: (val: T) => T
12): T {
13 return processor(value);
14}
15
16const processedString = process("hello", (s) => s.toUpperCase());
17const processedNumber = process(42, (n) => n * 2);Generic Constraints and Conditional Types:
1// Generic constraints
2interface Lengthwise {
3 length: number;
4}
5
6function logLength<T extends Lengthwise>(arg: T): T {
7 console.log(arg.length);
8 return arg;
9}
10
11logLength("hello"); // Works: string has length
12logLength([1, 2, 3]); // Works: array has length
13// logLength(123); // Error: number doesn't have length
14
15// Conditional return types
16function createArray<T>(length: number): T extends string ? string[] : T extends number ? number[] : unknown[] {
17 return Array(length).fill(null) as any;
18}Utility Generic Functions:
1// Generic array utilities
2function first<T>(array: T[]): T | undefined {
3 return array[0];
4}
5
6function last<T>(array: T[]): T | undefined {
7 return array[array.length - 1];
8}
9
10function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
11 return array.filter(predicate);
12}
13
14// Generic object utilities
15function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
16 const result = {} as Pick<T, K>;
17 keys.forEach(key => {
18 if (key in obj) {
19 result[key] = obj[key];
20 }
21 });
22 return result;
23}
24
25const user = { id: 1, name: "John", email: "[email protected]", age: 30 };
26const basicInfo = pick(user, "name", "email"); // Type: { name: string; email: string }TypeScript excels at typing higher-order functions and functional programming patterns, enabling sophisticated function composition and transformation.
Higher-Order Function Types:
1// Functions that take functions as parameters
2type Predicate<T> = (item: T) => boolean;
3type Transformer<T, U> = (item: T) => U;
4type Reducer<T, U> = (accumulator: U, current: T) => U;
5
6function filterMap<T, U>(
7 array: T[],
8 predicate: Predicate<T>,
9 transformer: Transformer<T, U>
10): U[] {
11 return array.filter(predicate).map(transformer);
12}
13
14// Usage
15const numbers = [1, 2, 3, 4, 5];
16const evenSquares = filterMap(
17 numbers,
18 (n) => n % 2 === 0,
19 (n) => n * n
20); // [4, 16]Function Composition:
1// Compose functions with proper typing
2function compose<T, U, V>(
3 f: (x: U) => V,
4 g: (x: T) => U
5): (x: T) => V {
6 return (x: T) => f(g(x));
7}
8
9// Pipe functions for better readability
10function pipe<T>(...fns: Array<(arg: T) => T>): (value: T) => T {
11 return (value: T) => fns.reduce((acc, fn) => fn(acc), value);
12}
13
14// Usage
15const addOne = (x: number) => x + 1;
16const double = (x: number) => x * 2;
17const stringify = (x: number) => x.toString();
18
19const composedFunction = compose(stringify, compose(double, addOne));
20const pipedFunction = pipe(addOne, double);
21
22console.log(composedFunction(5)); // "12"
23console.log(pipedFunction(5)); // 12Currying and Partial Application:
1// Curried function types
2type CurriedFunction2<T, U, R> = (arg1: T) => (arg2: U) => R;
3type CurriedFunction3<T, U, V, R> = (arg1: T) => (arg2: U) => (arg3: V) => R;
4
5function curry<T, U, R>(fn: (arg1: T, arg2: U) => R): CurriedFunction2<T, U, R> {
6 return (arg1: T) => (arg2: U) => fn(arg1, arg2);
7}
8
9// Partial application
10function partial<T, U, R>(fn: (arg1: T, arg2: U) => R, arg1: T): (arg2: U) => R {
11 return (arg2: U) => fn(arg1, arg2);
12}
13
14// Usage
15const add = (a: number, b: number): number => a + b;
16const curriedAdd = curry(add);
17const addFive = curriedAdd(5);
18
19console.log(addFive(3)); // 8
20
21const multiply = (a: number, b: number): number => a * b;
22const multiplyByTwo = partial(multiply, 2);
23console.log(multiplyByTwo(5)); // 10Event Handlers and Callbacks:
1// Event handler types
2type EventHandler<T = Event> = (event: T) => void;
3type AsyncEventHandler<T = Event> = (event: T) => Promise<void>;
4
5// Generic event emitter
6class TypedEventEmitter<TEvents extends Record<string, any[]>> {
7 private listeners: { [K in keyof TEvents]?: Array<(...args: TEvents[K]) => void> } = {};
8
9 on<K extends keyof TEvents>(event: K, listener: (...args: TEvents[K]) => void): void {
10 if (!this.listeners[event]) {
11 this.listeners[event] = [];
12 }
13 this.listeners[event]!.push(listener);
14 }
15
16 emit<K extends keyof TEvents>(event: K, ...args: TEvents[K]): void {
17 const eventListeners = this.listeners[event];
18 if (eventListeners) {
19 eventListeners.forEach(listener => listener(...args));
20 }
21 }
22}
23
24// Usage with typed events
25interface UserEvents {
26 login: [userId: string, timestamp: Date];
27 logout: [userId: string];
28 error: [message: string, code: number];
29}
30
31const userEmitter = new TypedEventEmitter<UserEvents>();
32
33userEmitter.on('login', (userId, timestamp) => {
34 console.log(`User ${userId} logged in at ${timestamp}`);
35});
36
37userEmitter.emit('login', 'user123', new Date());Async Function Patterns:
1// Async function utilities
2async function retry<T>(
3 fn: () => Promise<T>,
4 maxAttempts: number = 3,
5 delay: number = 1000
6): Promise<T> {
7 let lastError: Error;
8
9 for (let attempt = 1; attempt <= maxAttempts; attempt++) {
10 try {
11 return await fn();
12 } catch (error) {
13 lastError = error as Error;
14 if (attempt < maxAttempts) {
15 await new Promise(resolve => setTimeout(resolve, delay));
16 }
17 }
18 }
19
20 throw lastError!;
21}
22
23// Promise utilities
24function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
25 return Promise.race([
26 promise,
27 new Promise<never>((_, reject) =>
28 setTimeout(() => reject(new Error('Timeout')), ms)
29 )
30 ]);
31}
32
33// Usage
34const fetchData = async (): Promise<{ data: string }> => {
35 const response = await fetch('/api/data');
36 return response.json();
37};
38
39const dataWithRetry = await retry(fetchData, 3, 2000);
40const dataWithTimeout = await timeout(fetchData(), 5000);Your progress is saved in this browser, on this device only.