TypeScript Generics and Utility Types
Generics let you write code that works with many types, while still keeping full type safety.
The Problem
Suppose you want a function that returns the first item of any array. With any, you lose the type:
Example
function firstAny(list: any[]): any {
return list[0];
}
const x = firstAny(["Karachi", "Lahore"]); // x is any: no help, no checksA Generic Function
A generic function has a type parameter, written in angle brackets, often called T. It is like a variable for a type.
Example
function first<T>(list: T[]): T | undefined {
return list[0];
}
const city = first(["Karachi", "Lahore"]); // city: string | undefined
const mark = first([88, 92, 75]); // mark: number | undefined
console.log(city, mark);Karachi 88
TypeScript works out T from the argument you pass. You can also write it yourself: first<string>(["a", "b"]).
More Than One Type Parameter
Example
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const p = pair("rollNo", 117); // [string, number]
console.log(p);[ 'rollNo', 117 ]
Generic Interfaces
Generics are very common for describing data that wraps other data, like an API response:
Example
interface ApiResponse<T> {
ok: boolean;
data: T;
}
interface Course {
title: string;
months: number;
}
const res: ApiResponse<Course[]> = {
ok: true,
data: [
{ title: "Web Development", months: 4 },
{ title: "Graphic Designing", months: 3 }
]
};
console.log(res.data.map(c => c.title).join(", "));Web Development, Graphic Designing
The same ApiResponse works for users, orders or anything else.
Constraints With extends
Sometimes T must have certain properties. T extends ... adds a constraint:
Example
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(longest("Hyderabad", "Sukkur"));
console.log(longest([1, 2], [1, 2, 3]));
// longest(10, 20); // Error: number does not have a 'length' propertyHyderabad [ 1, 2, 3 ]
A Generic Class
Example
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
size(): number {
return this.items.length;
}
}
const plates = new Stack<string>();
plates.push("Red plate");
plates.push("Blue plate");
console.log(plates.pop(), plates.size());Blue plate 1
Utility Types
TypeScript comes with ready-made generic types that change other types:
| Utility type | Result |
|---|---|
Partial<T> | All properties of T become optional |
Required<T> | All properties become required |
Readonly<T> | All properties become readonly |
Pick<T, "a" OR "b"> | Only the listed properties |
Omit<T, "a"> | All properties except the listed ones |
Record<K, V> | An object with keys K and values V |
(In real code, the keys in Pick and Omit are joined with the | union symbol.)
Example
interface User {
id: number;
name: string;
email: string;
password: string;
}
type UserUpdate = Partial<User>;
type PublicUser = Omit<User, "password">;
type UserCard = Pick<User, "name" | "email">;
const update: UserUpdate = { name: "Ayesha Khan" };
const card: UserCard = { name: "Ayesha Khan", email: "ayesha@example.com" };
const fees: Record<string, number> = { html: 3000, javascript: 5000 };
console.log(update, card.email, fees.javascript);{ name: 'Ayesha Khan' } ayesha@example.com 5000Name type parameters clearly when there are several: TKey and TValue are easier to follow than K and V in long code.
Exercise
Exercise
What is the return type of first(["a", "b"]) if function first<T>(list: T[]): T?
