Bits Kingdom logo with a hexagon lattice, uppercase text in white, and a minimalistic design.

Strictly Typed: Mastering Functions and Generics in TypeScript

From Zero to Production-Ready Code - Chapter 3

by Aug 4, 2025Development

Home / Development / Strictly Typed: Mastering Functions and Generics in TypeScript

In my experience, one of the most powerful aspects of TypeScript is how effortlessly it enhances function safety and clarity. Typing functions doesn’t just catch errors—it fundamentally changes the way you approach code reuse and maintainability. In this chapter, I’ll guide you through mastering function typing, managing optional and default parameters, handling function overloads, and leveraging generics to craft reusable, robust, and elegant code.

1. Typing Functions in TypeScript: Essential Basics

Clearly defining input and output types of functions makes your code predictable, safe, and easy to debug.

Explicit parameter and return types:

function add(x: number, y: number): number {
  return x + y;
}

Arrow function syntax:

const greet = (name: string): string => `Hello, ${name}!`;

Type inference:

If you omit the return type, TypeScript infers it:

function multiply(a: number, b: number) {
  return a * b; // inferred as number
}

2. Flexible Functions: Optional, Default, and Rest Parameters in TypeScript

3D TypeScript logo for a series of posts.

TypeScript lets you define functions flexibly using optional, default, and rest parameters: perfect for versatile, maintainable functions.

Optional parameters:

Mark with a ?—must come after required parameters.

function log(message: string, userId?: string) {
  console.log(userId ? `[${userId}] ${message}` : message);
}

Default parameters:

function pow(base: number, exponent: number = 2) {
  return base ** exponent;
}

Rest parameters:

Gather multiple arguments into an array.

function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}

3. Defining Function Types and Call Signatures in TypeScript

Discover how TypeScript allows you to clearly define the shape and behavior of function types using call signatures.

You can describe a function’s type using type aliases or interfaces:

type MathOp = (a: number, b: number) => number;

const divide: MathOp = (a, b) => a / b;

Using interfaces:

interface Formatter {
  (value: string, upper?: boolean): string;
}

const format: Formatter = (value, upper) =>
  upper ? value.toUpperCase() : value.toLowerCase();

4. TypeScript Function Overloads: One Function, Multiple Signatures

Function overloads enable a single function to handle multiple signatures, adapting seamlessly to different scenarios.

function reverse(input: string): string;
function reverse(input: number[]): number[];
function reverse(input: any): any {
  if (typeof input === "string") {
    return input.split("").reverse().join("");
  }
  if (Array.isArray(input)) {
    return input.slice().reverse();
  }
}

const reversedString = reverse("hello"); // string
const reversedArray = reverse([1, 2, 3]); // number[]

5. Using Generics in TypeScript: Creating Reusable, Type-Safe Functions

Generics are the secret to creating highly reusable and strongly typed functions, arrays, and objects that work reliably across your entire codebase.

Basic example:

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

const str = identity("hello"); // T is string
const num = identity(123);     // T is number

Generic type aliases:

type Pair<T> = [T, T];

const coordinates: Pair<number> = [10, 20];
const names: Pair<string> = ["Ada", "Grace"];

6. Advanced Generics in TypeScript: Constraints for Enhanced Flexibility

Generics become even more powerful when you use constraints—ensuring flexibility while enforcing required structure.

You can restrict which types can be used with generics using extends.

function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

getLength([1, 2, 3]);        // OK, arrays have length
getLength("hello");          // OK, strings have length
// getLength(123);           // Error, number has no length

7. Real-World Examples of TypeScript Use

Generic React Props (for blogs covering React):

type ListProps<T> = {
  items: T[];
  render: (item: T) => React.ReactNode;
};

Utility function with multiple generics:

function mapObject<K extends string, V, U>(
  obj: Record<K, V>,
  fn: (value: V) => U
): Record<K, U> {
  const result: any = {};
  for (const key in obj) {
    result[key] = fn(obj[key]);
  }
  return result;
}

Quick Recap

  • Typed Functions: Essential for clarity, reliability, and bug prevention.
  • Flexible Parameters: Use optional, default, and rest parameters for greater adaptability.
  • Function Overloads: Enable a single function to clearly support multiple use cases.
  • Generics: Crucial for building reusable, scalable, and type-safe code.
  • Constraints: Enhance generics by enforcing necessary structures without losing flexibility.

Next up: Object-Oriented TypeScript and Advanced Typing Patterns. Learn how to combine classes, advanced types, and thoughtful architecture to build powerful, maintainable applications!

About the author

<a href="https://bitskingdom.com/blog/author/maria/" target="_self">Maria Nario</a>
Maria Nario
As a co-founder of BitsKingdom and a Bachelor of Science in Communication, I bring years of experience as a copywriter to everything I do. I’ve spent my career building connections through words. Now, I juggle a variety of moving parts while maintaining a sense of calm and focus, even when it feels like the world is falling apart.

Explore more topics: