Learn TypeScript: Generics

You may want to read this article in Bahasa Indonesia version: Mengenal TypeScript: Generics
Previously we have briefly discussed the use of Generics in Mengenal TypeScript: Interface & Type and Mengenal TypeScript: Type Assertions, Type Narrowing, & Function, but this time we will discuss in more detail about what Generics are in TypeScript. TypeScript Generics is a feature that allows us to create components that can work with multiple data types, not just one type. This article will explain the concept, syntax, and practical use of Generics in TypeScript.
What is Generics?
Generics allow us to create functions, classes, or interfaces that can work with a variety of data types while still maintaining type safety. With Generics, we can write code that is more flexible, reusable, and still type-safe.
Simple Generics Functions
Let’s start with a simple generic function example:
/** Generic function that returns the same argument */
function identity<T>(arg: T): T {
return arg;
}
/** Using function with explicit type */
const output1 = identity<string>("Hello"); // output1 is of type string
const output2 = identity<number>(42); // output2 is of type number
/** TypeScript can also infer the type automatically */
const output3 = identity("World"); // output3 is of type string
const output4 = identity(100); // output4 is of type number
Pada contoh di atas, T
adalah parameter tipe yang akan digantikan dengan tipe aktual saat fungsi dipanggil. Fungsi identity
akan menerima argumen bertipe T
dan mengembalikan nilai bertipe T
juga.
Multiple Type Parameters
Generics can also use more than one type parameter:
/** Function with two type parameters */
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result1 = pair<string, number>("key", 42); // [string, number]
const result2 = pair<boolean, string[]>(true, ["a"]); // [boolean, string[]]
/** With type inference */
const result3 = pair("hello", 100); // [string, number]
Generic Constraints
Sometimes we want to limit the types that can be used with generics. This can be done with generic constraints:
/** Interface for constraint */
interface HasLength {
length: number;
}
/** Function that only accepts types with length property */
function logLength<T extends HasLength>(arg: T): T {
console.log(`Length: ${arg.length}`);
return arg;
}
/** Valid because string has length property */
logLength("Hello"); // Length: 5
/** Valid because array has length property */
logLength([1, 2, 3]); // Length: 3
/** Valid because object has length property */
logLength({ length: 10 }); // Length: 10
/** Error: number does not have length property */
// logLength(42);
With the constraint T extends HasLength
, we limit type T
only to types that have a length
property of type number
.
Generic Interface and Type
Generics can also be used with interfaces and type aliases:
/** Generic interface */
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "Hello" };
const numberBox: Box<number> = { value: 42 };
/** Generic type alias */
type Pair<T, U> = {
first: T;
second: U;
};
const keyValue: Pair<string, number> = { first: "key", second: 42 };
Generic Classes
We can also create generic classes:
/** Generic class */
class Container<T> {
private item: T;
constructor(item: T) {
this.item = item;
}
getItem(): T {
return this.item;
}
setItem(item: T): void {
this.item = item;
}
}
const numberContainer = new Container<number>(123);
console.log(numberContainer.getItem()); // 123
const stringContainer = new Container<string>("Hello");
console.log(stringContainer.getItem()); // "Hello"
Default Type Parameters
Generics can also have default values:
/** Interface with default type parameter */
interface ApiResponse<T = any> {
data: T;
status: number;
message: string;
}
/** No need to specify type */
const response1: ApiResponse = {
data: { name: "John" },
status: 200,
message: "Success",
};
/** Explicitly specifying type */
interface User {
id: number;
name: string;
}
const response2: ApiResponse<User> = {
data: { id: 1, name: "John" },
status: 200,
message: "Success",
};
Generic Type Inference
TypeScript can infer generic types in many cases:
/** Generic function with inference */
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
/** TypeScript infers T as string */
const first = firstElement(["a", "b", "c"]); // first: string | undefined
/** TypeScript infers T as number */
const second = firstElement([1, 2, 3]); // second: number | undefined
/** TypeScript infers T as union type */
const mixed = firstElement([1, "a", true]); // mixed: string | number | boolean | undefined
Practical Use Cases
Here are some examples of the use of generics that are often encountered when we create applications using TypeScript.
Generic Utility Functions
/** Generic map function */
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
return array.map(fn);
}
const numbers = [1, 2, 3, 4];
const doubled = map(numbers, (n) => n * 2); // [2, 4, 6, 8]
const strings = map(numbers, (n) => n.toString()); // ["1", "2", "3", "4"]
const booleans = map(numbers, (n) => n % 2 === 0); // [false, true, false, true]
Component Wrapper (React-style)
interface Props<T> {
data: T;
render: (item: T) => string;
}
function Wrapper<T>(props: Props<T>): string {
return props.render(props.data);
}
const numberWrapper = Wrapper({
data: 42,
render: (num) => `Number: ${num}`,
});
const userWrapper = Wrapper({
data: { name: "John", age: 30 },
render: (user) => `User: ${user.name}, Age: ${user.age}`,
});
console.log(numberWrapper); // "Number: 42"
console.log(userWrapper); // "User: John, Age: 30"
Type-safe API Calls
/** Generic API function */
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json() as Promise<T>;
}
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
/** Type-safe API calls */
async function loadData() {
const user = await fetchData<User>("https://api.example.com/user/1");
console.log(user.name); // TypeScript knows user has name property
const product = await fetchData<Product>("https://api.example.com/product/1");
console.log(product.price); // TypeScript knows product has price property
}
Keypoint Generics
Here are some key points about Generics:
-
Reusability
Generics allow us to write code that can be reused with different data types.
-
Type Safety
Despite being flexible, generics maintain type safety.
-
Better Inference
TypeScript can infer generic types in many cases.
-
Constraints
Generics make code more expressive and self-documenting.
-
Readability
Generics make code more expressive and self-documenting.
Conclusion
Generics are a powerful feature in TypeScript that allow us to create flexible yet type-safe components. By understanding and leveraging generics, we can write code that is more reusable, expressive, and safe. The functional programming approach becomes more powerful when combined with generics, allowing us to create utility functions that can work with various data types without sacrificing type safety.