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.
- A typed function describes the shapes of its inputs and outputs.
- A generic function uses a type parameter such as T to keep relationships between values.
- Generics help you avoid any while keeping code reusable.
- They are common in helper functions, data processing, and API wrappers.
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:
- utility functions such as first, merge, or pluck
- data transformation functions that keep input and output types related
- callbacks and event handlers with exact parameter shapes
- library code that should work for many consumers
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:
- a fetch helper that parses JSON into a known result type
- a reusable validator that accepts different data shapes
- sorting and filtering helpers for lists of objects
- collection utilities such as map, reduce, and group operations
- shared library functions that should work for many applications
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 rejectedThe 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
- Generics are erased at runtime, so you cannot inspect T in real JavaScript code after compilation.
- TypeScript infers generic types from usage, but inference can fail for empty arrays or ambiguous values.
- Callbacks may need explicit parameter types when context is not enough for inference.
- Functions with many generic parameters can become difficult to read if the API is not designed carefully.
- Not every reusable function needs a generic; a simple union or a fixed type is sometimes clearer.
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
- TypeScript functions add type safety to normal JavaScript functions.
- Generics make a function reusable across different types without losing type information.
- Type parameters such as T connect inputs and outputs.
- Constraints like extends should be used only when the function needs them.
- Good generic design improves autocomplete, refactoring, and bug prevention.
11. Practice Exercise
- Write a generic function named wrapInArray that accepts one value and returns it inside a one-item array.
- Call it with a string, a number, and an object.
- Make sure the returned array keeps the correct item type in each case.
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.