Generics basics
In this chapter, you learn about generics, one of TypeScript's signature features.
What you learn in this chapter
- What generics are and why they are needed
- How to write generic functions
- How to use type parameters
- How type inference works
Once you understand generics, you can write reusable, type-safe code. It may feel difficult at first, but let's deepen your understanding through real code examples.
What are generics
Generics are a mechanism that takes a type as a "parameter." This lets you create reusable components that work with various types.
Why generics are needed
First, let's look at the problem when you do not use generics.
// You have to write a function with the same logic for each type
function getFirstNumber(arr: number[]): number {
return arr[0];
}
function getFirstString(arr: string[]): string {
return arr[0];
}
function getFirstBoolean(arr: boolean[]): boolean {
return arr[0];
}
// Usage
console.log(getFirstNumber([1, 2, 3])); // 1
console.log(getFirstString(["a", "b"])); // "a"
console.log(getFirstBoolean([true, false])); // true
Problems:
- You have to write the same logic (return the first element of an array) for each type
- You need a new function every time you support a new type
- Code duplication increases
The problem with using the any type
// Using the any type loses type safety
function getFirstAny(arr: any[]): any {
return arr[0];
}
const num = getFirstAny([1, 2, 3]);
// num is the any type → type information is lost
// num.toUpperCase(); // runtime error (numbers do not have toUpperCase)
Problems:
- The return value becomes the
anytype, so type checking does not work - Type safety is lost, which becomes a source of bugs
The solution with generics
// A generic function
// <T> is a type parameter (the concrete type is decided at call time)
function getFirst<T>(arr: T[]): T {
return arr[0];
}
// Specify the type parameter explicitly
const num = getFirst<number>([1, 2, 3]); // number type
const str = getFirst<string>(["a", "b"]); // string type
const bool = getFirst<boolean>([true, false]); // boolean type
// Type inference lets you omit the type parameter
const num2 = getFirst([1, 2, 3]); // number type (inferred)
const str2 = getFirst(["a", "b"]); // string type (inferred)
Benefits:
- A single function handles any type
- Type safety is preserved
- Code duplication disappears
Three steps to write a generic function
Let's learn how to start from a concrete type and gradually convert it into a generic function.
// Step 1: first write a function that works with a concrete type
function getFirstNumber(arr: number[]): number | undefined {
return arr[0];
}
// Step 2: replace the type part with a "type parameter"
// Replace number with T
// Add <T> after the function name
function getFirstGeneric<T>(arr: T[]): T | undefined {
return arr[0];
}
// Step 3: use it and check the type inference
const num = getFirstGeneric([1, 2, 3]); // number | undefined ✓
const str = getFirstGeneric(["a", "b", "c"]); // string | undefined ✓
const obj = getFirstGeneric([{ id: 1 }]); // { id: number } | undefined ✓
Key points:
- First write working code: implement the function with a concrete type (number, string, etc.)
- Abstract the type: replace the concrete type with
Tand add<T>to the function - Check type inference: verify that the type is inferred correctly at call time
When you are "unsure where to replace with T", look for the part that should be the same type in both the input and the output. For example, for "return an array element", the input (the array's element type) and the output (the return value) should be the same.
Generic functions
Basic syntax
// For a function declaration
function functionName<TypeParameter>(arg: type): returnType {
// Process
}
// For an arrow function
const functionName = <TypeParameter>(arg: type): returnType => {
// Process
};
The identity function (a basic example)
// A function that returns the received value as is
function identity<T>(value: T): T {
return value;
}
// Usage
const num = identity<number>(42); // number type
const str = identity<string>("Hello"); // string type
const obj = identity<{ name: string }>({ name: "Taro" }); // { name: string } type
// Using type inference
const num2 = identity(42); // number type (inferred)
const str2 = identity("Hello"); // string type (inferred)
Functions that handle arrays
// Get the last element of an array
function getLast<T>(arr: T[]): T | undefined {
if (arr.length === 0) {
return undefined;
}
return arr[arr.length - 1];
}
console.log(getLast([1, 2, 3])); // 3
console.log(getLast(["a", "b", "c"])); // "c"
console.log(getLast([])); // undefined
// Reverse an array
function reverse<T>(arr: T[]): T[] {
return [...arr].reverse();
}
console.log(reverse([1, 2, 3])); // [3, 2, 1]
console.log(reverse(["a", "b", "c"])); // ["c", "b", "a"]
Wrapping a value in an array
// Wrap a value in an array
function wrapInArray<T>(value: T): T[] {
return [value];
}
console.log(wrapInArray(42)); // [42]
console.log(wrapInArray("hello")); // ["hello"]
console.log(wrapInArray({ x: 1 })); // [{ x: 1 }]
Type parameters
Naming conventions for type parameters
By convention, a single uppercase letter is used for a type parameter.
| Parameter name | Meaning | Example use |
|---|---|---|
T | Type | A general type |
K | Key | An object's key |
V | Value | An object's value |
E | Element | An array's element |
R | Result | A return type |
// An example with multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p1 = pair<number, string>(1, "one"); // [number, string]
const p2 = pair<string, boolean>("flag", true); // [string, boolean]
// Using type inference
const p3 = pair(42, "answer"); // [number, string]
Multiple type parameters
// Create a key-value pair
function createEntry<K, V>(key: K, value: V): { key: K; value: V } {
return { key, value };
}
const entry1 = createEntry("name", "Taro");
// { key: string; value: string }
const entry2 = createEntry(1, { name: "Taro" });
// { key: number; value: { name: string } }
// A map function
function map<T, R>(arr: T[], fn: (item: T) => R): R[] {
return arr.map(fn);
}
const numbers = [1, 2, 3];
const doubled = map(numbers, n => n * 2); // number[]
const strings = map(numbers, n => n.toString()); // string[]
Type inference
TypeScript can automatically infer a type parameter from the arguments.
Basic type inference
function identity<T>(value: T): T {
return value;
}
// Specify the type parameter explicitly
const a = identity<number>(42);
// Type inference (inferred from the argument)
const b = identity(42); // T is inferred as number
// Inference works with multiple type parameters too
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const c = pair(42, "hello");
// T is inferred as number, U as string
When type inference works and when it does not
// Inference works: the type can be inferred from the argument
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}
const first = getFirst([1, 2, 3]); // T is inferred as number
// Inference does not work: the argument has no type information
function createEmpty<T>(): T[] {
return [];
}
// const arr = createEmpty(); // not an error, but T is inferred as unknown (unknown[])
const arr = createEmpty<string>(); // an explicit specification is needed
Type inference in callback functions
// Type inference in a callback function
function processArray<T, R>(arr: T[], processor: (item: T) => R): R[] {
return arr.map(processor);
}
const numbers = [1, 2, 3];
// The processor's argument item is inferred as the number type
const doubled = processArray(numbers, item => item * 2);
// item: number, return value: number[]
const strings = processArray(numbers, item => `Number: ${item}`);
// item: number, return value: string[]
Generics in arrow functions
Basic syntax
// Generics in an arrow function
const identity = <T>(value: T): T => {
return value;
};
// Multiple type parameters
const pair = <T, U>(first: T, second: U): [T, U] => {
return [first, second];
};
A note for TSX (React)
// In a TSX file, <T> may be interpreted as a JSX tag
// Avoid it by adding a comma
const identity = <T,>(value: T): T => {
return value;
};
// Or use extends
const identity2 = <T extends unknown>(value: T): T => {
return value;
};
Generics in object methods
const utils = {
// Use generics in a method
toArray<T>(value: T): T[] {
return [value];
},
repeat<T>(value: T, count: number): T[] {
return Array(count).fill(value);
},
map<T, R>(arr: T[], fn: (item: T) => R): R[] {
return arr.map(fn);
}
};
console.log(utils.toArray(42)); // [42]
console.log(utils.toArray("hello")); // ["hello"]
console.log(utils.repeat("x", 5)); // ["x", "x", "x", "x", "x"]
console.log(utils.map([1, 2, 3], n => n * 2)); // [2, 4, 6]
A practical example: general-purpose array operations
// A filter function
function filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {
const result: T[] = [];
for (const item of arr) {
if (predicate(item)) {
result.push(item);
}
}
return result;
}
const numbers = [1, 2, 3, 4, 5];
const evens = filter(numbers, n => n % 2 === 0); // [2, 4]
const words = ["apple", "banana", "cherry"];
const longWords = filter(words, w => w.length > 5); // ["banana", "cherry"]
// A find function
function find<T>(arr: T[], predicate: (item: T) => boolean): T | undefined {
for (const item of arr) {
if (predicate(item)) {
return item;
}
}
return undefined;
}
const found = find(numbers, n => n > 3); // 4
// A groupBy function
function groupBy<T, K extends string | number>(
arr: T[],
getKey: (item: T) => K
): Record<K, T[]> {
const result = {} as Record<K, T[]>;
for (const item of arr) {
const key = getKey(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
}
return result;
}
const people = [
{ name: "Taro", age: 20 },
{ name: "Hanako", age: 20 },
{ name: "Jiro", age: 30 }
];
const byAge = groupBy(people, p => p.age);
// { 20: [{name: "Taro"...}, {name: "Hanako"...}], 30: [{name: "Jiro"...}] }
Try it: build general-purpose array operation functions ★★
Implement the following functions.
Requirements:
unique<T>: return a new array with duplicates removedchunk<T>: return a 2D array by splitting the array into the given sizezip<T, U>: combine two arrays and return an array of tuples
// Implement the following functions
// function unique<T>(arr: T[]): T[]
// function chunk<T>(arr: T[], size: number): T[][]
// function zip<T, U>(arr1: T[], arr2: U[]): [T, U][]
Hint
unique: aSetcan remove duplicateschunk: useslicein a loop to split the arrayzip: use the length of the shorter array as the basis
Answer and explanation
// Remove duplicates
function unique<T>(arr: T[]): T[] {
return [...new Set(arr)];
}
// Split an array into the given size
function chunk<T>(arr: T[], size: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}
// Combine two arrays
function zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
const length = Math.min(arr1.length, arr2.length);
const result: [T, U][] = [];
for (let i = 0; i < length; i++) {
result.push([arr1[i], arr2[i]]);
}
return result;
}
// Test
console.log(unique([1, 2, 2, 3, 3, 3]));
// [1, 2, 3]
console.log(unique(["a", "b", "a", "c"]));
// ["a", "b", "c"]
console.log(chunk([1, 2, 3, 4, 5], 2));
// [[1, 2], [3, 4], [5]]
console.log(chunk(["a", "b", "c", "d"], 3));
// [["a", "b", "c"], ["d"]]
console.log(zip([1, 2, 3], ["a", "b", "c"]));
// [[1, "a"], [2, "b"], [3, "c"]]
console.log(zip([1, 2], ["a", "b", "c", "d"]));
// [[1, "a"], [2, "b"]] (cut to the length of the shorter array)
Explanation:
unique: aSetremoves duplicates automatically. Spread syntax turns it back into an arraychunk:slice(i, i + size)cuts out a portion of the array and pushes it into the result arrayzip: uses the length of the shorter of the two arrays as the basis to combine corresponding elements into tuples
Thanks to generics, these functions can be used type-safely with arrays of any type.
Summary
What you learned in this chapter:
- What generics are: a mechanism that takes a type as a parameter
- Why they are needed: you can reuse code while preserving type safety
- Generic functions: define a type parameter with
<T> - Type parameters: naming conventions such as
T,K,V - Type inference: the type parameter is automatically inferred from the arguments
In the next chapter, you will learn about generic constraints and generic classes.
Common pitfalls for beginners
Common mistakes
❌ Using a concrete type for the type parameter
// ❌ Using a concrete type name as the type parameter
function identity<string>(value: string): string {
return value; // this is the type parameter "string", not the string type
}
// ✅ Use a conventional name
function identityCorrect<T>(value: T): T {
return value;
}
Cause: <string> is "a type parameter named string", not the built-in string type.
Solution: Use conventional names like T, U, K, V for type parameters.
❌ Relying too much on type inference and getting an unintended type
// ❌ The type of an empty array cannot be inferred
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}
const result = getFirst([]); // T is inferred as never, so the result is never | undefined
// ✅ Specify the type parameter explicitly
const result2 = getFirst<string>([]); // string | undefined
Cause: The element type cannot be inferred from an empty array, so it becomes the never type.
Solution: When the type cannot be inferred, specify the type parameter explicitly.
❌ Accessing a property of the type parameter inside generics
// ❌ T does not necessarily have a specific property
function getLength<T>(value: T): number {
return value.length; // Error: Property 'length' does not exist on type 'T'
}
// ✅ Add a constraint (covered in detail in the next chapter)
function getLengthSafe<T extends { length: number }>(value: T): number {
return value.length; // OK
}
Cause: A type parameter T could be anything, so the existence of a specific property is not guaranteed.
Solution: Add a constraint with extends, or consider a design that does not use a type parameter.
❌ Generics in an arrow function cause an error in TSX
// ❌ In a TSX file, <T> is mistaken for JSX
const identity = <T>(value: T): T => value;
// Error: JSX element 'T' has no corresponding closing tag.
// ✅ Approach 1: add a comma
const identity1 = <T,>(value: T): T => value;
// ✅ Approach 2: add extends
const identity2 = <T extends unknown>(value: T): T => value;
Cause: In a TSX file, <T> is interpreted as a JSX tag.
Solution: Add a comma like <T,>, or add a constraint like <T extends unknown>.
❌ Forgetting to define the relationship between multiple type parameters
// ❌ Without a relationship, it is not type-safe
function setProperty<T, K, V>(obj: T, key: K, value: V): void {
(obj as any)[key as any] = value; // not type-safe
}
// ✅ Define a relationship between the type parameters
function setPropertySafe<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
const user = { name: "Taro", age: 30 };
setPropertySafe(user, "name", "Jiro"); // OK
// setPropertySafe(user, "name", 123); // Error: a string is required
Cause: Without a relationship between type parameters, type checking does not work.
Solution: Define a relationship between type parameters with K extends keyof T or T[K].
const type parameters (TS 5.0+)
Normally, when you pass an array or object literal to a generic function, the type parameter is inferred as a "wide type" (e.g., string[]). When you declare <const T>, you get the same effect as passing the value with as const, and literal types are preserved.
// ❌ Normal generics: T is inferred as string[]
function createList<T>(items: T[]): T[] {
return items
}
const list1 = createList(['a', 'b', 'c'])
// the type of list1: string[]
// ✅ const type parameter: T is inferred as a literal type
function createConstList<const T>(items: T[]): T[] {
return items
}
const list2 = createConstList(['a', 'b', 'c'])
// the type of list2: readonly ['a', 'b', 'c']
A practical example: inferring tuple types
// A function that receives values while preserving the tuple element types
function tuple<const T extends readonly unknown[]>(...args: T): T {
return args
}
const pair = tuple('hello', 42)
// the type of pair: readonly ['hello', 42]
// previously it would widen to [string, number]
<const T> lets the caller get the same effect without writing as const, so it is especially useful when designing a library API. On the other hand, when you need to handle the data mutably internally, use the traditional <T>.
About default values for type parameters
You can also give a type parameter a default value (like <T = string>). This is covered in detail in the next chapter (Chapter 14) along with constraints.