TypeScript Functions & Generics: Syntax, Examples, and Patterns

TypeScript functions and generics let you write reusable code that is still strongly typed. In this article, you will learn how to type function parameters and return values, how generic type parameters work, and how to use them to build safer utilities without losing flexibility.

Quick answer: Use function types to describe what a function accepts and returns. Use generics when the same function should work with different types while keeping those types connected and checked by the compiler.

Difficulty: Beginner to Intermediate

You'll understand this better if you know: basic JavaScript functions, variables, and simple TypeScript types such as string, number, and Array.

1. What Is TypeScript Functions & Generics?

TypeScript functions are JavaScript functions with type annotations. Generics are a TypeScript feature that lets a function, type, or class work with more than one data type while preserving type information.

For example, a regular function can say “I accept a number and return a string,” while a generic function can say “I accept one value and return that same kind of value.”

2. Why TypeScript Functions & Generics Matter

Functions are the core building block of JavaScript, and TypeScript becomes most valuable when it can describe function behavior precisely. Generics matter because many real functions do not work with just one specific type.

Without generics, you often end up duplicating code for strings, numbers, and objects, or using overly broad types that hide mistakes until runtime. With generics, the compiler can verify that values passed through a function stay consistent.

This is especially useful when building:

3. Basic Syntax or Core Idea

Start with a normal typed function. Then add a type parameter when the return type or parameter type should stay linked to the input.

3.1 A typed function

This example shows a function that accepts a string and returns a number:

function countLetters(text: string): number {
  return text.length;
}

The parameter type comes after the parameter name, and the return type comes after the closing parenthesis. TypeScript checks both sides for consistency.

3.2 A generic function

This example accepts a value and returns the same value type:

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

The <T> part declares a type parameter. TypeScript fills in T based on how you call the function.

4. Step-by-Step Examples

4.1 Example: preserving a string type

When you call a generic function with a string, TypeScript infers T as string:

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

const username = identity("ada");

Here, username is inferred as string, not string | number or any. That means later string operations are safe.

4.2 Example: working with arrays

Generics are very useful for array helpers that should work with any item type:

function firstItem<T>(items: Array<T>): T | undefined {
  return items[0];
}

const firstName = firstItem(["Mina", "Omar"]);
const firstScore = firstItem([10, 20]);

TypeScript infers T separately for each call, so the function stays flexible and safe.

4.3 Example: multiple type parameters

Some functions relate two different types, such as a key and a value:

function pair<K, V>(key: K, value: V): Object {
  return { key, value };
}

Using more than one type parameter is common when the inputs do not share the same type.

4.4 Example: inferring return types from input

Generics can preserve object shapes too:

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

const profile = cloneValue({ name: "Lina", active: true });

The returned value keeps the same object shape that went in, which is much better than falling back to an untyped object.

5. Practical Use Cases

Typed functions and generics show up in everyday code. Common uses include:

For example, a generic API wrapper can preserve the response type so the caller gets proper autocomplete and compile-time checking.

6. Common Mistakes

Mistake 1: Using any instead of a generic

Beginners often reach for any because it makes the function easy to write. The problem is that it removes type safety and hides bad calls.

Problem: The function accepts and returns any, so TypeScript cannot catch mistakes such as calling string methods on a number result.

function echo(value: any): any {
  return value;
}

const result = echo(123);
result.toUpperCase();

Fix: Use a generic so the input and output stay connected.

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

const result = echo(123);
// result is number, so string-only methods are rejected

The generic version keeps the exact type, so invalid usage is caught earlier.

Mistake 2: Forgetting to type the return value when it matters

TypeScript can infer many return types, but inference can become too broad in more complex functions. That can lead to surprising types later.

Problem: This function returns a union-like object shape, and the caller may not get the level of precision they expect if the return type is not described clearly.

function makeStatus(ok: boolean) {
  if (ok) {
    return { status: "success", code: 200 };
  }

  return { status: "error", code: 500 };
}

Fix: Add an explicit return type when you want the function contract to stay clear.

type Status = {
  status: "success" | "error";
  code: number;
};

function makeStatus(ok: boolean): Status {
  if (ok) {
    return { status: "success", code: 200 };
  }

  return { status: "error", code: 500 };
}

The explicit return type documents the contract and prevents accidental changes later.

Mistake 3: Over-constraining a generic

Sometimes developers add too many restrictions and make the function harder to use than necessary.

Problem: This version forces every value to have an id property even when the function does not need one.

function wrap<T extends { id: string }>(value: T): T {
  return value;
}

Fix: Use a constraint only when the function actually depends on that property.

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

Keep constraints as narrow as possible so the function stays reusable.

7. Best Practices

7.1 Let TypeScript infer types when the code is obvious

When the implementation is simple, inference keeps the code shorter and easier to read.

const double = (value: number) => value * 2;

Use explicit types when they add clarity, but do not repeat information that TypeScript already knows.

7.2 Prefer meaningful generic names

T is common for a single generic, but names like TItem or TResponse can improve readability in larger codebases.

function takeFirst<TItem>(items: Array<TItem>): TItem | undefined {
  return items[0];
}

Readable generic names help other developers understand what the type parameter represents.

7.3 Use constraints only when needed

A constraint such as extends { id: string } is useful when the function must access a property. Otherwise, it narrows the function too much.

function getId<T extends { id: string }>(item: T): string {
  return item.id;
}

This is the right time for a constraint because the function actually relies on id.

8. Limitations and Edge Cases

Problem: A generic type parameter only exists in TypeScript's type system, so you cannot branch on it at runtime as if it were a real value.

That is why generic functions should use ordinary JavaScript logic and let the type system validate the shape, not the runtime type parameter itself.

9. Practical Mini Project

Let's build a small typed data helper for a list of products. The goal is to write one generic function that can pick a property from any array of objects.

type Product = {
  id: number;
  name: string;
  price: number;
};

function pluck<T, K extends keyof T>(
  items: Array<T>,
  key: K
): Array<T[K]> {
  return items.map((item) => item[key]);
}

const products: Array<Product> = [
  { id: 1, name: "Keyboard", price: 79 },
  { id: 2, name: "Mouse", price: 29 }
];

const names = pluck(products, "name");
const prices = pluck(products, "price");

This project shows the main strength of generics: the function stays reusable, but the return type is still exact. If you try to pass a key that does not exist, TypeScript will reject it at compile time.

10. Key Points

11. Practice Exercise

Expected output: the function should return arrays such as ["hello"], [42], and a typed object array like [{ id: 1 }].

Hint: Use one generic type parameter and return Array<T>.

function wrapInArray<T>(value: T): Array<T> {
  return [value];
}

const textList = wrapInArray("hello");
const numberList = wrapInArray(42);
const objectList = wrapInArray({ id: 1 });

12. Final Summary

TypeScript functions give you typed, reliable building blocks for everyday JavaScript code. By annotating parameters and return values, you make your intent clear and help the compiler catch mistakes before they become runtime bugs.

Generics take that idea further by letting one function work across many types while preserving the relationship between inputs and outputs. They are especially valuable for helpers, collections, and reusable library code where any would be too loose and duplicate implementations would be too costly.

If you want to continue, the next best step is to learn generic constraints, utility types, and function overloading so you can design even more expressive TypeScript APIs.