Function Types and Advanced Function Features
1. Function Type Expressions and Call Signatures
| Pattern | Syntax | Description | Use Case |
|---|---|---|---|
| Function Type Expression | (args) => ReturnType |
Inline function type - arrow syntax | Callbacks, function parameters |
| Call Signature | { (args): ReturnType } |
Function type in object/interface - can have properties | Callable objects with properties |
| Type Alias for Function | type Fn = (args) => Type |
Reusable named function type | Shared function signatures |
| Generic Function Type | <T>(arg: T) => T |
Function type with type parameters | Generic callbacks, transformers |
| Void Return | () => void |
Function returns nothing useful | Event handlers, side effects |
Example: Function type expressions
// Function type expression
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const multiply: MathOperation = (a, b) => a * b;
// Function as parameter
function calculate(
x: number,
y: number,
operation: (a: number, b: number) => number
): number {
return operation(x, y);
}
console.log(calculate(5, 3, add)); // 8
console.log(calculate(5, 3, multiply)); // 15
// Generic function type
type Mapper<T, U> = (item: T) => U;
const toString: Mapper<number, string> = (n) => n.toString();
const toLength: Mapper<string, number> = (s) => s.length;
// Complex function types
type Predicate<T> = (item: T) => boolean;
type Transformer<T, U> = (input: T) => U;
type Handler = (event: Event) => void;
type AsyncFetcher<T> = (url: string) => Promise<T>;
Example: Call signatures
// Call signature with properties
interface Callable {
(x: number): string; // Call signature
description: string; // Property
version: number; // Property
}
function createCallable(): Callable {
const fn = ((x: number) => x.toString()) as Callable;
fn.description = "Converts number to string";
fn.version = 1;
return fn;
}
const callable = createCallable();
console.log(callable(42)); // "42"
console.log(callable.description); // "Converts number to string"
// Multiple call signatures (overloads)
interface Formatter {
(value: string): string;
(value: number): string;
(value: boolean): string;
}
const format: Formatter = (value: any): string => {
return String(value);
};
// Constructor signature
interface Constructor {
new (name: string): Object;
}
// Hybrid type - both callable and constructible
interface DateConstructor {
new (value: number): Date;
new (value: string): Date;
(): string;
now(): number;
}
2. Function Overloading and Implementation Signatures
| Component | Description | Purpose |
|---|---|---|
| Overload Signatures | Multiple function signatures without implementation | Declare all valid call patterns |
| Implementation Signature | Single signature with function body - must be compatible with all overloads | Actual function implementation |
| Return Type Variation | Different return types based on parameters | Type-safe conditional returns |
| Parameter Variation | Different number/types of parameters | Flexible function API |
Example: Function overloading
// Basic overloading
function getValue(id: number): string;
function getValue(name: string): number;
function getValue(value: number | string): string | number {
if (typeof value === "number") {
return `ID: ${value}`;
} else {
return value.length;
}
}
let result1 = getValue(123); // string
let result2 = getValue("Alice"); // number
// Overloading with different parameter counts
function createElement(tag: string): HTMLElement;
function createElement(tag: string, content: string): HTMLElement;
function createElement(tag: string, content: string, className: string): HTMLElement;
function createElement(tag: string, content?: string, className?: string): HTMLElement {
const element = document.createElement(tag);
if (content) element.textContent = content;
if (className) element.className = className;
return element;
}
// Overloading with generics
function parse(value: string): any;
function parse<T>(value: string, reviver: (key: string, value: any) => any): T;
function parse<T>(value: string, reviver?: (key: string, value: any) => any): T | any {
return JSON.parse(value, reviver);
}
// Overloading for conditional return types
function process(input: string): string;
function process(input: number): number;
function process(input: boolean): boolean;
function process(input: string | number | boolean): string | number | boolean {
return input;
}
let str = process("hello"); // string
let num = process(42); // number
let bool = process(true); // boolean
Example: Advanced overloading patterns
// Overloading with union narrowing
function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;
function makeDate(yearOrTimestamp: number, month?: number, day?: number): Date {
if (month !== undefined && day !== undefined) {
return new Date(yearOrTimestamp, month - 1, day);
} else {
return new Date(yearOrTimestamp);
}
}
const date1 = makeDate(1609459200000); // Date from timestamp
const date2 = makeDate(2024, 1, 1); // Date from parts
// Overloading with type predicates
function filter<T>(array: T[], predicate: (item: T) => boolean): T[];
function filter<T, S extends T>(
array: T[],
predicate: (item: T) => item is S
): S[];
function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
return array.filter(predicate);
}
// Method overloading in classes
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: any, b: any): any {
return a + b;
}
}
const calc = new Calculator();
calc.add(1, 2); // 3 (number)
calc.add("a", "b"); // "ab" (string)
3. Optional Parameters and Default Parameter Values
| Feature | Syntax | Description | Behavior |
|---|---|---|---|
| Optional Parameter | param?: Type |
Parameter may be undefined or omitted | Type is Type | undefined |
| Default Parameter | param: Type = value |
Parameter has default value if not provided | Type inferred from default |
| Optional After Required | Optional must come after required params | TypeScript enforces parameter order | Compile-time enforcement |
| Destructured Defaults | { x = 1, y = 2 } |
Default values in object destructuring | Individual property defaults |
Example: Optional parameters
// Basic optional parameters
function greet(name: string, greeting?: string): string {
if (greeting) {
return `${greeting}, ${name}!`;
}
return `Hello, ${name}!`;
}
console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet("Bob", "Hi")); // "Hi, Bob!"
// Multiple optional parameters
function createUser(
name: string,
age?: number,
email?: string
): { name: string; age?: number; email?: string } {
return { name, age, email };
}
createUser("Alice"); // { name: "Alice" }
createUser("Bob", 30); // { name: "Bob", age: 30 }
createUser("Charlie", 25, "c@email.com"); // All fields
// Optional with type checking
function log(message: string, level?: "info" | "warn" | "error"): void {
const prefix = level ? `[${level.toUpperCase()}]` : "[LOG]";
console.log(`${prefix} ${message}`);
}
Example: Default parameters
// Basic default parameters
function power(base: number, exponent: number = 2): number {
return Math.pow(base, exponent);
}
console.log(power(3)); // 9 (3^2)
console.log(power(3, 3)); // 27 (3^3)
// Type inferred from default
function createArray(length = 10) { // length: number
return new Array(length);
}
// Complex default values
function fetchData(
url: string,
options: RequestInit = { method: "GET", headers: {} }
): Promise<Response> {
return fetch(url, options);
}
// Default with destructuring
function drawRect({
x = 0,
y = 0,
width = 100,
height = 100,
color = "black"
}: {
x?: number;
y?: number;
width?: number;
height?: number;
color?: string;
} = {}): void {
console.log(`Drawing ${width}x${height} rect at (${x},${y})`);
}
drawRect(); // All defaults
drawRect({ x: 10, y: 20 }); // Partial values
drawRect({ width: 200, color: "red" }); // Mix defaults and values
// Default with type alias
type Options = {
timeout?: number;
retries?: number;
};
function request(url: string, { timeout = 5000, retries = 3 }: Options = {}): void {
console.log(`Request to ${url} (timeout: ${timeout}, retries: ${retries})`);
}
4. Rest Parameters with Spread Syntax
| Feature | Syntax | Description | Type |
|---|---|---|---|
| Rest Parameters | ...args: Type[] |
Capture variable number of arguments as array | Array of Type |
| Typed Rest | ...items: T[] |
Rest parameter with specific type | Type-safe varargs |
| Tuple Rest | ...args: [T, U, V] |
Rest parameter as tuple type | Fixed types for each position |
| Rest Position | Must be last parameter | Only one rest parameter, at the end | Language constraint |
| Spread in Call | fn(...array) |
Spread array as individual arguments | Array to arguments |
Example: Rest parameters
// Basic rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
// Rest with required parameters
function multiply(multiplier: number, ...numbers: number[]): number[] {
return numbers.map(n => n * multiplier);
}
console.log(multiply(2, 1, 2, 3)); // [2, 4, 6]
// Generic rest parameters
function merge<T>(...objects: T[]): T {
return Object.assign({}, ...objects);
}
const result = merge({ a: 1 }, { b: 2 }, { c: 3 });
// { a: 1, b: 2, c: 3 }
// Rest with different types
function logAll(prefix: string, ...messages: (string | number)[]): void {
messages.forEach(msg => console.log(`${prefix}: ${msg}`));
}
logAll("INFO", "Starting", 123, "Complete");
// Tuple rest parameters
function process(...args: [string, number, boolean]): void {
const [name, age, active] = args;
console.log(`${name} is ${age} years old, active: ${active}`);
}
process("Alice", 30, true);
Example: Spread syntax
// Spread array to function arguments
const numbers = [1, 2, 3, 4, 5];
console.log(Math.max(...numbers)); // 5
// Spread with typed function
function greet(first: string, second: string, third: string): void {
console.log(`${first}, ${second}, ${third}`);
}
const names: [string, string, string] = ["Alice", "Bob", "Charlie"];
greet(...names); // Type-safe spread
// Rest in function type
type Logger = (...messages: string[]) => void;
const log: Logger = (...messages) => {
console.log(messages.join(" "));
};
// Combining rest and spread
function combineArrays<T>(...arrays: T[][]): T[] {
return arrays.flat();
}
const combined = combineArrays([1, 2], [3, 4], [5, 6]);
// [1, 2, 3, 4, 5, 6]
// Rest parameters with constraints
function createPair<T extends string | number>(...items: [T, T]): [T, T] {
return items;
}
const pair1 = createPair("a", "b"); // [string, string]
const pair2 = createPair(1, 2); // [number, number]
5. this Parameter and Explicit this Types
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| this Parameter | function(this: Type) |
Explicit type for this context - not a real parameter |
Method typing, callbacks |
| this Type | this: void |
Function doesn't use this or requires no specific context |
Standalone functions |
| ThisParameterType<T> | Extract this parameter type |
Get the type of this from function type |
Type utilities |
| OmitThisParameter<T> | Remove this from function type |
Convert method to standalone function type | Function transformations |
| noImplicitThis | Compiler flag | Require explicit this types - prevents errors |
Strict type checking |
Example: Explicit this parameter
// this parameter in function
interface User {
name: string;
greet(this: User): void;
}
const user: User = {
name: "Alice",
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
user.greet(); // OK - correct context
// const greetFn = user.greet;
// greetFn(); // Error if noImplicitThis enabled
// this in callbacks
interface Database {
query(this: Database, sql: string): void;
}
const db: Database = {
query(sql: string) {
console.log(`Executing: ${sql}`);
}
};
// Standalone function with this
function logThis(this: { value: number }): void {
console.log(this.value);
}
const obj = { value: 42 };
logThis.call(obj); // 42
// this: void - no this context needed
function calculate(this: void, a: number, b: number): number {
// Cannot use 'this' here
return a + b;
}
// Generic this
interface Builder<T> {
setValue(this: Builder<T>, value: T): this;
build(this: Builder<T>): T;
}
class StringBuilder implements Builder<string> {
private value: string = "";
setValue(value: string): this {
this.value = value;
return this;
}
build(): string {
return this.value;
}
}
Example: this type utilities
// ThisParameterType - extract this type
type ObjectMethod = (this: { name: string }, age: number) => void;
type ThisType = ThisParameterType<ObjectMethod>;
// Result: { name: string }
// OmitThisParameter - remove this parameter
type StandaloneMethod = OmitThisParameter<ObjectMethod>;
// Result: (age: number) => void
// Practical use case
interface Context {
prefix: string;
}
function log(this: Context, message: string): void {
console.log(`${this.prefix}: ${message}`);
}
// Extract this type for type safety
type LogContext = ThisParameterType<typeof log>;
const context: LogContext = { prefix: "INFO" };
log.call(context, "Starting");
// Convert to standalone
type StandaloneLog = OmitThisParameter<typeof log>;
const standaloneFn: StandaloneLog = (message) => {
console.log(message);
};
// this in class methods
class Component {
private state: number = 0;
// Method has implicit 'this: Component'
increment(): void {
this.state++;
}
// Bind-safe method with this parameter
onClick(this: Component, event: Event): void {
this.increment();
}
}
6. Arrow Functions and Lexical this Binding
| Feature | Regular Function | Arrow Function |
|---|---|---|
| this Binding | Dynamic - depends on call site | Lexical - captured from enclosing scope |
| arguments | Has arguments object |
No arguments - use rest params |
| Constructor | Can be used with new |
Cannot be constructor |
| Syntax | function() { } |
() => { } or () => expr |
| prototype | Has prototype property |
No prototype |
Example: Arrow function this binding
// Arrow functions capture 'this' lexically
class Counter {
count: number = 0;
// Regular method - 'this' is dynamic
increment() {
this.count++;
}
// Arrow function - 'this' is lexical (captured)
delayedIncrement = () => {
setTimeout(() => {
this.count++; // 'this' refers to Counter instance
}, 1000);
};
// Problem with regular function
problematicIncrement() {
setTimeout(function() {
// this.count++; // Error: 'this' is undefined or window
}, 1000);
}
}
const counter = new Counter();
counter.delayedIncrement(); // Works correctly
// Arrow functions in event handlers
class Button {
label: string = "Click me";
// Arrow function preserves 'this'
handleClick = (event: MouseEvent) => {
console.log(`${this.label} was clicked`);
};
attachHandler(element: HTMLElement) {
element.addEventListener("click", this.handleClick);
}
}
// Array methods with arrow functions
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);
Example: Arrow function patterns
// Implicit return
const square = (x: number) => x * x;
const greet = (name: string) => `Hello, ${name}!`;
// Object literal return (wrap in parens)
const makePoint = (x: number, y: number) => ({ x, y });
// Generic arrow functions
const identity = <T>(value: T): T => value;
const toArray = <T>(value: T): T[] => [value];
// Arrow function as callback
setTimeout(() => console.log("Done"), 1000);
// Arrow with destructuring
const printUser = ({ name, age }: { name: string; age: number }) => {
console.log(`${name} is ${age} years old`);
};
// Async arrow function
const fetchUser = async (id: number): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
// Higher-order arrow functions
const multiply = (x: number) => (y: number) => x * y;
const double = multiply(2);
console.log(double(5)); // 10
// Arrow vs regular in class
class Example {
name = "Example";
// Regular method - shared on prototype
regularMethod() {
return this.name;
}
// Arrow function - unique per instance
arrowMethod = () => {
return this.name;
};
}
// Arrow preserves context in callbacks
class DataLoader {
data: string[] = [];
load(urls: string[]) {
// 'this' preserved in arrow function
urls.forEach(url => {
fetch(url).then(response => {
this.data.push(response.url); // Safe to use 'this'
});
});
}
}
Note: Use arrow functions when you need to preserve
this context (event handlers, callbacks). Use regular functions for
methods that might be overridden or when you need arguments object.
Function Best Practices
- Use function type expressions for inline callback types
- Apply overloading for multiple valid signatures
- Prefer default parameters over optional with undefined checks
- Use rest parameters for variable-length argument lists
- Add explicit this parameter when context matters
- Use arrow functions for callbacks needing lexical this
- Enable
noImplicitThisfor safer this usage