Data report"State of code review 2024" is now liveRead the full report

TypeScript generics

Kenny DuMez
Kenny DuMez
Graphite software engineer

Generics in TypeScript provide a way to create reusable components that work with a variety of data types while preserving type safety. By using generics, you can create functions, classes, interfaces, and other structures that can operate on different types without sacrificing the benefits of strong typing. This guide will explore the various aspects of TypeScript generics, including generic functions, types, interfaces, and more.

Generics allow you to define a placeholder for a type that can be specified later. This is done using angle bracket syntax (<>) with a type parameter.

Terminal
function identity<T>(arg: T): T {
return arg
}
let result = identity<string>('Hello, TypeScript')
console.log(result) // Output: Hello, TypeScript

In this example, T is a type parameter that represents an argument to the function. When calling identity, we specify that T should be a string.

A generic function is a function that can work with any data type. You define a generic function by adding a type parameter in angle brackets before the function's parameters.

Terminal
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length) // Array has a .length property, so no error
return arg
}
let result = loggingIdentity<number>([1, 2, 3])
console.log(result) // Output: [1, 2, 3]

Here, loggingIdentity is a generic function that takes an array of type T and logs its length. The function returns the same array.

Generic types are types that take one or more type parameters. This allows you to define a type that can be reused with different data types.

Terminal
interface GenericIdentityFn<T> {
(arg: T): T
}
function identity<T>(arg: T): T {
return arg
}
let myIdentity: GenericIdentityFn<number> = identity
console.log(myIdentity(10)) // Output: 10

In this example, GenericIdentityFn is a generic interface that represents a function accepting and returning a value of type T. We then create a variable myIdentity of this type, specifying number as the type parameter.

Arrow functions in TypeScript can also be generic. You define a generic arrow function by adding a type parameter before the function's parameters.

Terminal
const genericArrowFunction = <T>(arg: T): T => {
return arg
}
let result = genericArrowFunction<string>('Hello, arrow functions')
console.log(result) // Output: Hello, arrow functions

In this example, genericArrowFunction is a generic arrow function that takes a value of type T and returns the same value.

You can define generic objects by using generic interfaces or type aliases.

Terminal
interface Box<T> {
contents: T
}
let stringBox: Box<string> = { contents: 'Hello, World' }
console.log(stringBox.contents) // Output: Hello, World
let numberBox: Box<number> = { contents: 100 }
console.log(numberBox.contents) // Output: 100

In this example, Box is a generic interface that represents an object with a contents property of type T. We then create instances of Box with different types.

Generic interfaces allow you to define a contract that can be applied to various types.

Terminal
interface KeyValuePair<K, V> {
key: K
value: V
}
let kvp: KeyValuePair<string, number> = { key: 'Age', value: 30 }
console.log(kvp.key) // Output: Age
console.log(kvp.value) // Output: 30

Here, KeyValuePair is a generic interface with two type parameters, K and V. We create an instance of KeyValuePair with string as the key type and number as the value type.

TypeScript also supports optional generics, where you can provide default types for type parameters.

Terminal
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value)
}
let stringArray = createArray(3, 'Hello')
console.log(stringArray) // Output: ["Hello", "Hello", "Hello"]
let numberArray = createArray<number>(3, 42)
console.log(numberArray) // Output: [42, 42, 42]

In this example, the createArray function has a generic type parameter T with a default type of string. If no type is specified, T defaults to string.

Sometimes, you need to constrain the types that can be used as generic parameters. This is done using the extends keyword.

Terminal
function getLength<T extends { length: number }>(arg: T): number {
return arg.length
}
console.log(getLength('Hello')) // Output: 5
console.log(getLength([1, 2, 3, 4])) // Output: 4

In this example, T is constrained to types that have a length property. This allows the function to access the length property without causing a type error.

Generics can also be used in classes to create flexible and reusable components.

Terminal
class DataStorage<T> {
private data: T[] = []
addItem(item: T) {
this.data.push(item)
}
removeItem(item: T) {
this.data = this.data.filter((i) => i !== item)
}
getItems() {
return [...this.data]
}
}
let textStorage = new DataStorage<string>()
textStorage.addItem('Hello')
textStorage.addItem('World')
textStorage.removeItem('Hello')
console.log(textStorage.getItems()) // Output: ["World"]
let numberStorage = new DataStorage<number>()
numberStorage.addItem(10)
numberStorage.addItem(20)
numberStorage.removeItem(10)
console.log(numberStorage.getItems()) // Output: [20]

In this example, DataStorage is a generic class that can store items of any type. The type is specified when creating an instance of the class.

For further reading on generic types in TypeScript, see the official documentation.

Stay unblocked. Ship faster.
Experience the new developer workflow - create, review, and merge code continuously. Get started with one command.
Learn more

Give your PR workflow
an upgrade today

Stack easier | Ship smaller | Review quicker

Or install our CLI.
Product Screenshot 1
Product Screenshot 2