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 noImplicitThis for safer this usage