Back to Blog
TypeScriptGenericsTypesFundamentals

TypeScript Generics Basics Guide

Learn TypeScript generics for creating flexible, reusable, type-safe code.

B
Bootspring Team
Engineering
August 20, 2018
7 min read

Generics allow you to create reusable components that work with multiple types while maintaining type safety.

Basic Generic Syntax#

1// Generic function 2function identity<T>(value: T): T { 3 return value; 4} 5 6// TypeScript infers the type 7identity('hello'); // T is string 8identity(42); // T is number 9identity(true); // T is boolean 10 11// Explicit type argument 12identity<string>('hello'); 13identity<number>(42); 14 15// Generic arrow function 16const identityArrow = <T>(value: T): T => value; 17 18// Note: In JSX files, use trailing comma 19const identityJSX = <T,>(value: T): T => value;

Generic Functions#

1// Swap function 2function swap<T, U>(pair: [T, U]): [U, T] { 3 return [pair[1], pair[0]]; 4} 5 6swap([1, 'hello']); // [string, number] 7swap(['a', true]); // [boolean, string] 8 9// First element 10function first<T>(array: T[]): T | undefined { 11 return array[0]; 12} 13 14first([1, 2, 3]); // number | undefined 15first(['a', 'b']); // string | undefined 16 17// Map function 18function map<T, U>(array: T[], fn: (item: T) => U): U[] { 19 return array.map(fn); 20} 21 22map([1, 2, 3], n => n.toString()); // string[] 23map(['a', 'b'], s => s.length); // number[]

Generic Interfaces#

1// Generic interface 2interface Container<T> { 3 value: T; 4 getValue(): T; 5} 6 7const stringContainer: Container<string> = { 8 value: 'hello', 9 getValue() { return this.value; } 10}; 11 12const numberContainer: Container<number> = { 13 value: 42, 14 getValue() { return this.value; } 15}; 16 17// Generic interface with methods 18interface Repository<T> { 19 getById(id: string): T | undefined; 20 getAll(): T[]; 21 save(item: T): void; 22 delete(id: string): boolean; 23} 24 25// Implementation 26interface User { 27 id: string; 28 name: string; 29} 30 31class UserRepository implements Repository<User> { 32 private users: User[] = []; 33 34 getById(id: string): User | undefined { 35 return this.users.find(u => u.id === id); 36 } 37 38 getAll(): User[] { 39 return [...this.users]; 40 } 41 42 save(user: User): void { 43 this.users.push(user); 44 } 45 46 delete(id: string): boolean { 47 const index = this.users.findIndex(u => u.id === id); 48 if (index >= 0) { 49 this.users.splice(index, 1); 50 return true; 51 } 52 return false; 53 } 54}

Generic Classes#

1class Box<T> { 2 private value: T; 3 4 constructor(value: T) { 5 this.value = value; 6 } 7 8 getValue(): T { 9 return this.value; 10 } 11 12 setValue(value: T): void { 13 this.value = value; 14 } 15} 16 17const stringBox = new Box('hello'); 18stringBox.getValue(); // string 19 20const numberBox = new Box(42); 21numberBox.getValue(); // number 22 23// Multiple type parameters 24class Pair<T, U> { 25 constructor( 26 public first: T, 27 public second: U 28 ) {} 29 30 swap(): Pair<U, T> { 31 return new Pair(this.second, this.first); 32 } 33} 34 35const pair = new Pair('hello', 42); 36const swapped = pair.swap(); // Pair<number, string>

Generic Constraints#

1// Constrain to types with length 2interface HasLength { 3 length: number; 4} 5 6function logLength<T extends HasLength>(item: T): number { 7 console.log(item.length); 8 return item.length; 9} 10 11logLength('hello'); // OK - string has length 12logLength([1, 2, 3]); // OK - array has length 13// logLength(42); // Error - number has no length 14 15// Constrain to object types 16function merge<T extends object, U extends object>(a: T, b: U): T & U { 17 return { ...a, ...b }; 18} 19 20merge({ name: 'John' }, { age: 30 }); 21// merge('hello', 'world'); // Error 22 23// Constrain to keys of another type 24function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { 25 return obj[key]; 26} 27 28const user = { name: 'John', age: 30 }; 29getProperty(user, 'name'); // string 30getProperty(user, 'age'); // number 31// getProperty(user, 'email'); // Error - 'email' not in keyof user

Default Type Parameters#

1// Default type 2interface ApiResponse<T = unknown> { 3 data: T; 4 status: number; 5 message: string; 6} 7 8// Uses default (unknown) 9const response1: ApiResponse = { 10 data: 'anything', 11 status: 200, 12 message: 'OK' 13}; 14 15// Explicit type 16const response2: ApiResponse<User> = { 17 data: { id: '1', name: 'John' }, 18 status: 200, 19 message: 'OK' 20}; 21 22// Multiple defaults 23interface Config<T = string, U = number> { 24 value: T; 25 count: U; 26} 27 28const config1: Config = { value: 'hello', count: 5 }; 29const config2: Config<boolean> = { value: true, count: 5 }; 30const config3: Config<boolean, string> = { value: true, count: 'five' };

Generic Type Aliases#

1// Simple alias 2type Nullable<T> = T | null; 3type Optional<T> = T | undefined; 4 5const name: Nullable<string> = null; 6const age: Optional<number> = undefined; 7 8// Array wrapper 9type ArrayOf<T> = T[]; 10const numbers: ArrayOf<number> = [1, 2, 3]; 11 12// Object with value 13type ValueHolder<T> = { value: T }; 14const holder: ValueHolder<string> = { value: 'hello' }; 15 16// Function type 17type Mapper<T, U> = (item: T) => U; 18const toString: Mapper<number, string> = n => n.toString(); 19 20// Conditional generic 21type NonNullable<T> = T extends null | undefined ? never : T; 22type Result = NonNullable<string | null>; // string

Generic Utility Types#

1// Built-in utility types use generics 2 3// Partial - make all properties optional 4interface User { 5 id: string; 6 name: string; 7 email: string; 8} 9 10type PartialUser = Partial<User>; 11// { id?: string; name?: string; email?: string } 12 13// Required - make all properties required 14interface Config { 15 host?: string; 16 port?: number; 17} 18 19type RequiredConfig = Required<Config>; 20// { host: string; port: number } 21 22// Pick - select specific properties 23type UserName = Pick<User, 'name'>; 24// { name: string } 25 26// Omit - exclude specific properties 27type UserWithoutId = Omit<User, 'id'>; 28// { name: string; email: string } 29 30// Record - create object type with keys 31type Roles = 'admin' | 'user' | 'guest'; 32type RolePermissions = Record<Roles, string[]>; 33// { admin: string[]; user: string[]; guest: string[] }

Generic Functions with Objects#

1// Create object from entries 2function fromEntries<K extends string, V>( 3 entries: [K, V][] 4): Record<K, V> { 5 return Object.fromEntries(entries) as Record<K, V>; 6} 7 8const obj = fromEntries([ 9 ['name', 'John'], 10 ['city', 'Boston'] 11]); 12// { name: string; city: string } 13 14// Pick properties 15function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { 16 const result = {} as Pick<T, K>; 17 keys.forEach(key => { 18 result[key] = obj[key]; 19 }); 20 return result; 21} 22 23const user = { id: 1, name: 'John', email: 'john@example.com' }; 24const nameOnly = pick(user, ['name']); 25// { name: string }

Generic React Components#

1// Generic list component 2interface ListProps<T> { 3 items: T[]; 4 renderItem: (item: T) => React.ReactNode; 5} 6 7function List<T>({ items, renderItem }: ListProps<T>) { 8 return <ul>{items.map(renderItem)}</ul>; 9} 10 11// Usage 12<List 13 items={[{ id: 1, name: 'John' }]} 14 renderItem={user => <li key={user.id}>{user.name}</li>} 15/> 16 17// Generic select component 18interface SelectProps<T> { 19 options: T[]; 20 value: T; 21 onChange: (value: T) => void; 22 getLabel: (option: T) => string; 23 getValue: (option: T) => string; 24} 25 26function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) { 27 return ( 28 <select 29 value={getValue(value)} 30 onChange={e => { 31 const selected = options.find(o => getValue(o) === e.target.value); 32 if (selected) onChange(selected); 33 }} 34 > 35 {options.map(option => ( 36 <option key={getValue(option)} value={getValue(option)}> 37 {getLabel(option)} 38 </option> 39 ))} 40 </select> 41 ); 42}

Common Patterns#

1// Factory function 2function createStore<T>(initial: T) { 3 let state = initial; 4 5 return { 6 get: () => state, 7 set: (value: T) => { state = value; }, 8 update: (updater: (current: T) => T) => { 9 state = updater(state); 10 } 11 }; 12} 13 14const numberStore = createStore(0); 15numberStore.set(5); 16numberStore.update(n => n + 1); 17 18// Builder pattern 19class QueryBuilder<T> { 20 private query: Partial<T> = {}; 21 22 where<K extends keyof T>(key: K, value: T[K]): this { 23 this.query[key] = value; 24 return this; 25 } 26 27 build(): Partial<T> { 28 return { ...this.query }; 29 } 30} 31 32interface User { 33 name: string; 34 age: number; 35 active: boolean; 36} 37 38const query = new QueryBuilder<User>() 39 .where('name', 'John') 40 .where('active', true) 41 .build();

Best Practices#

Naming Conventions: ✓ T for single type parameter ✓ K for key type ✓ V for value type ✓ Descriptive names for complex cases Constraints: ✓ Use extends for constraints ✓ Prefer specific constraints ✓ Document constraint requirements ✓ Use keyof for property access Design: ✓ Start simple, add generics when needed ✓ Let TypeScript infer when possible ✓ Use defaults for common cases ✓ Keep type parameters minimal Avoid: ✗ Unnecessary generics ✗ Too many type parameters ✗ Overly complex constraints ✗ Any as escape hatch

Conclusion#

Generics are fundamental to TypeScript for creating reusable, type-safe code. Start with simple type parameters and add constraints as needed. Use built-in utility types like Partial, Pick, and Record to transform types. Let TypeScript infer types when possible, and provide explicit type arguments only when necessary for clarity or disambiguation.

Share this article

Help spread the word about Bootspring