Functions & Generics: Reusable Type Safety

Functions & Generics: Reusable Type Safety

Functions are the primary means of passing data around in any application. TypeScript adds a layer of safety to these data transitions by ensuring that inputs (parameters) and outputs (return values) match exactly what you expect. Generics take this a step further by allowing you to write code that works with any type while still maintaining strict safety.


1. Typing Functions

In TypeScript, you should explicitly type your parameters and your return value.

Standard and Arrow Functions

// Standard function
function add(a: number, b: number): number {
    return a + b;
}

// Arrow function
const multiply = (x: number, y: number): number => x * y;

Optional and Default Parameters

  • Optional (?): Parameters that can be omitted.
  • Default (=): Parameters that have a fallback value if not provided.
function greet(name: string, title?: string): string {
    return title ? `Hello, ${title} ${name}` : `Hello, ${name}`;
}

function power(base: number, exponent: number = 2): number {
    return Math.pow(base, exponent);
}

2. Function Overloads

Sometimes, a function can be called in multiple ways with different types of arguments. You can define multiple "overload signatures" followed by one "implementation signature."

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
    if (d !== undefined && y !== undefined) {
        return new Date(y, mOrTimestamp, d);
    } else {
        return new Date(mOrTimestamp);
    }
}

3. Generics: The "Type Variable"

Generics allow you to create components that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

The Problem:

If we use any, we lose the type information:

function identity(arg: any): any { return arg; }
let res = identity("Hello"); // 'res' is type 'any'

The Solution:

We use a Type Variable (usually <T>) to capture the type the user provides:

function identity<T>(arg: T): T {
    return arg;
}

let output = identity<string>("Hello"); // TypeScript knows 'output' is a 'string'
let numOutput = identity(100); // TypeScript infers T is 'number'

4. Generic Constraints

Sometimes you want to use a Generic, but you need to ensure the type has certain properties (e.g., it must have a .length property). You can use the extends keyword to add a Constraint.

interface HasLength {
    length: number;
}

function logLength<T extends HasLength>(item: T): void {
    console.log(item.length);
}

logLength("Hello"); // OK: Strings have .length
logLength([1, 2, 3]); // OK: Arrays have .length
// logLength(10); // Error: Numbers do not have .length

5. Generic Classes and Interfaces

Generics aren't just for functions; they are essential for data structures.

interface ApiResponse<T> {
    data: T;
    status: number;
    error?: string;
}

const userResponse: ApiResponse<{ name: string }> = {
    data: { name: "Alice" },
    status: 200
};

6. Summary

  • Type your functions to prevent "undefined is not a function" errors.
  • Use Generics when you want to write logic that is independent of the specific data type being processed.
  • Use Constraints to limit what types can be passed to your Generics.