TypeScript: TypeScript Generics Basics
Last updated: 2026-08-26
Generics are TypeScript’s most powerful feature—they allow you to write code that “works with multiple types” while maintaining full type safety. Understanding generics is a key step in mastering TypeScript.
1. Why Are Generics Needed?
(1) The Dilemma of Not Having Generics
TYPESCRIPT
// Option 1: Use any — missing type information
function firstAny(arr: any[]): any {
return arr[0];
}
let n = firstAny([1, 2, 3]); // n is any — don't know if it's number
let s = firstAny(["a", "b"]); // s is any — don't know if it's string
// Option 2:Write a function for each type——Code Duplication
function firstNumber(arr: number[]): number { return arr[0]; }
function firstString(arr: string[]): string { return arr[0]; }
// Endless...
(2) Generic Solutions
TYPESCRIPT
function first<T>(arr: T[]): T {
return arr[0];
}
let n = first([1, 2, 3]); // T Inferred as number → Back number
let s = first(["a", "b"]); // T Inferred as string → Back string
let b = first([true, false]); // T Inferred as boolean → Back boolean
console.log(n.toFixed(2)); // ✅ TypeScript knows n is number
console.log(s.toUpperCase()); // ✅ TypeScript knows s is string
📌 Key Concept:
<T> is a type parameter—what T is depends on the type of the argument passed when the function is called. It is defined once but can be used with multiple types, and precise type information is available for each case.
2. Generic Functions
(1) Basic Syntax
TYPESCRIPT
function identity<T>(value: T): T {
return value;
}
// TypeScript Automatic Inference T
let num = identity(42); // T = number
let str = identity("hello"); // T = string
// You can also specify it explicitly T(Sometimes it is necessary to)
let result = identity<string>("hello");
(2) Multiple type parameters
TYPESCRIPT
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
let p1 = pair("name", 42); // [string, number]
let p2 = pair(true, [1, 2, 3]); // [boolean, number[]]
(3) Generics and Arrays
TYPESCRIPT
function map<T, U>(arr: T[], transform: (item: T) => U): U[] {
return arr.map(transform);
}
let numbers = [1, 2, 3];
let strings = map(numbers, n => `No.${n}`);
// T = number, U = string → Back string[]
console.log(strings); // ["No.1", "No.2", "No.3"]
▶ Example: Generic Utility Functions
TYPESCRIPT
// Filter Array
function filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {
return arr.filter(predicate);
}
// Searching an Array
function find<T>(arr: T[], predicate: (item: T) => boolean): T | undefined {
return arr.find(predicate);
}
// Array Chunking
function chunk<T>(arr: T[], size: number): T[][] {
let result: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}
let nums = [1, 2, 3, 4, 5, 6, 7];
let evens = filter(nums, n => n % 2 === 0);
let found = find(nums, n => n > 5);
let groups = chunk(nums, 3);
console.log("Even number:" + evens); // "Even number:2,4,6"
console.log("Found:" + found); // "Found:6"
console.log("Chunking:" + JSON.stringify(groups)); // "[[1,2,3],[4,5,6],[7]]"
Output:
TEXT
📖 Display only
Even number:2,4,6
Found:6
Chunking:[[1,2,3],[4,5,6],[7]]
3. Generic Interfaces
(1) Basic Syntax
TYPESCRIPT
interface Box<T> {
value: T;
}
let stringBox: Box<string> = { value: "hello" };
let numberBox: Box<number> = { value: 42 };
(2) Generic Interface Description Function
TYPESCRIPT
interface Transformer<A, B> {
(input: A): B;
}
let toString: Transformer<number, string> = (n) => String(n);
let toLength: Transformer<string, number> = (s) => s.length;
console.log(toString(42)); // "42"
console.log(toLength("hello")); // 5
(3) The Generic Interface Description Repository Pattern
TYPESCRIPT
interface Repository<T> {
findById(id: string): T | null;
findAll(): T[];
save(item: T): void;
delete(id: string): boolean;
}
interface User {
id: string;
name: string;
}
class UserRepo implements Repository<User> {
private data: User[] = [];
findById(id: string): User | null {
return this.data.find(u => u.id === id) ?? null;
}
findAll(): User[] {
return this.data;
}
save(item: User): void {
this.data.push(item);
}
delete(id: string): boolean {
let len = this.data.length;
this.data = this.data.filter(u => u.id !== id);
return this.data.length < len;
}
}
4. Generic Classes
(1) Basic Syntax
TYPESCRIPT
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
let numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.push(3);
console.log(numStack.pop()); // 3
console.log(numStack.peek()); // 2
console.log(numStack.size); // 2
let strStack = new Stack<string>();
strStack.push("a");
strStack.push("b");
console.log(strStack.pop()); // "b"
(2) Static Members of Generic Classes
TYPESCRIPT
class GenericClass<T> {
// static defaultValue: T; // ❌ Static members cannot reference a class's type parameters
// ✅ Static methods require their own type parameters.
static create<U>(value: U): GenericClass<U> {
let instance = new GenericClass<U>(value);
return instance;
}
constructor(public value: T) {}
}
let instance = GenericClass.create("hello"); // GenericClass<string>
📌 Reason: The class's type parameter
T is instance-level—each instance can have a different T. Static members belong to the class itself, not to any specific instance, so they cannot reference an instance's type parameters.
5. Generic Constraints (extends)
By default, the generic type T can be any type—sometimes this is too broad. Use extends to constrain T so that it must satisfy certain conditions:
(1) Constrained to a specific interface
TYPESCRIPT
interface HasLength {
length: number;
}
// T Must have length Properties
function logLength<T extends HasLength>(value: T): void {
console.log(`Length:${value.length}`);
}
logLength("hello"); // ✅ string has length
logLength([1, 2, 3]); // ✅ The array contains length
logLength({ length: 10 }); // ✅ Objects include length
// logLength(42); // ❌ number None length
(2) A constraint is another generic
TYPESCRIPT
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let user = { name: "Charlie", age: 20, email: "xiao@example.com" };
console.log(getProperty(user, "name")); // ✅ "Charlie" —— K = "name"
console.log(getProperty(user, "age")); // ✅ 20 —— K = "age"
// getProperty(user, "phone"); // ❌ "phone" No user the key
(3) Constraints as Constructors
TYPESCRIPT
function createInstance<T>(Constructor: new () => T): T {
return new Constructor();
}
class Dog {
bark() { return "Woof!"; }
}
let dog = createInstance(Dog);
console.log(dog.bark()); // "Woof!"
▶ Example: Generic Function with keyof Constraint
TYPESCRIPT
function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
let result = {} as Pick<T, K>;
for (let key of keys) {
result[key] = obj[key];
}
return result;
}
let user = { id: 1, name: "Charlie", email: "c@example.com", age: 25 };
let summary = pick(user, "id", "name");
// Type: { id: number; name: string }
console.log(summary.name); // "Charlie"
// summary.email; // ❌ email was not picked
Output:
TEXT
📖 Display only
Charlie
▶ Example: Type-Safe Object Merging
TYPESCRIPT
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
let defaults = { host: "localhost", port: 3000 };
let custom = { port: 8080, debug: true };
let config = merge(defaults, custom);
// Inference Types:{ host: string; port: number } & { port: number; debug: boolean }
// After simplification:{ host: string; port: number; debug: boolean }
console.log(config.host); // "localhost"
console.log(config.port); // 8080
console.log(config.debug); // true
Output:
TEXT
📖 Display only
localhost
8080
true
6. Common Built-in Generic Types
TypeScript includes many useful generic utility types:
| Type | Function | Example |
|---|---|---|
Array<T> |
Array Type | Array<number> |
Promise<T> |
Promise Type | Promise<string> |
Record<K, V> |
Key-value pair object | Record<string, number> |
Partial<T> |
All attributes are optional | Partial<Config> |
Required<T> |
All fields are required | Required<Config> |
Readonly<T> |
All properties are read-only | Readonly<Config> |
Pick<T, K> |
Select certain attributes | Pick<User, "name" | "age"> |
Omit<T, K> |
Exclude certain attributes | Omit<User, "email"> |
Exclude<T, U> |
Exclude from Union Types | Exclude<"a"|"b"|"c", "a"> |
Extract<T, U> |
Extract from Union Types | Extract<"a"|"b"|"c", "a"|"b"> |
ReturnType<T> |
Function Return Type | ReturnType<typeof fn> |
Parameters<T> |
Tuple of Function Parameter Types | Parameters<typeof fn> |
TYPESCRIPT
// Examples of Actual Use
interface User {
id: number;
name: string;
email: string;
age: number;
}
// When creating a user id Not required
type CreateUser = Omit<User, "id">;
// All fields are optional when updating a user
type UpdateUser = Partial<User>;
// The user summary includes only some fields
type UserSummary = Pick<User, "id" | "name">;
let newUser: CreateUser = { name: "Charlie", email: "xiao@example.com", age: 20 };
let update: UpdateUser = { age: 21 };
let summary: UserSummary = { id: 1, name: "Charlie" };
❓ FAQ
Q How should generic type parameters be named?
A By convention, use a single uppercase letter—T (Type), U/V/W (subsequent types), K (Key), V (Value), E (Element). For multiple words, use PascalCase:
TItem, TResponse. Teams should adopt a consistent naming convention.Q When do you need to explicitly specify a generic type?
A In most cases, TypeScript can infer generic types from the parameters. You need to specify them explicitly in the following scenarios: (1) The function has no parameters, or the parameter types do not allow T to be determined; (2) The inference is too broad (e.g., inferred as a union type); (3) You need more precise control over the function’s return type.
Q What is the difference between the generic constraint
extends and class inheritance extends?A The syntax is the same, but the semantics differ. The generic constraint
T extends HasLength means “T must have at least the structure of HasLength”—it is a type compatibility relationship. Class inheritance class Dog extends Animal means “Dog is a subclass of Animal”—it is an object-oriented relationship.Q What is the difference between
Record<string, number> and { [key: string]: number }?A They are completely equivalent.
Record is a built-in generic utility type that is essentially an index signature. Record<string, number> is more concise, while { [key: string]: number } is more flexible (it allows you to add known properties).📖 Summary
- Generics
<T>are type parameters—they allow functions, interfaces, and classes to work with multiple types while maintaining type safety. - Generic function:
function fn<T>(arg: T): T, where T is inferred from the arguments at call time - Generic interfaces and generic classes: Add "T" to the end of the name,
interface Box<T>,class Stack<T> - Generic constraints
T extends typerestrict the range of T to ensure that T satisfies certain conditions - TypeScript includes a rich set of built-in generic utility types: Record, Partial, Required, Readonly, Pick, Omit, and others
📝 Exercises
- Basic Problem (Difficulty ⭐): Write a generic function
wrapInArray<T>(value: T): T[]that wraps a single value into an array. Test it withnumberandstringtypes. - Advanced Problem (Difficulty ⭐⭐): Write a generic function
pluck<T, K extends keyof T>(items: T[], key: K): T[K][]that extracts the value of a specified property from an array of objects. For example,pluck([{name: "a", age: 1}, {name: "b", age: 2}], "name")returns["a", "b"]. - Challenge (Difficulty: ⭐⭐⭐): Implement a generic class
DataStore<T extends { id: string }>that provides save, find, and delete methods. Ensure that the id property exists through generic constraints. Create an instance ofDataStore<{ id: string; title: string }>to test the CRUD functionality.