Generics in TypeScript

Generics in TypeScript

This article explains generics in TypeScript.

YouTube Video

Generics in TypeScript

Generics in TypeScript are a feature that allows you to define reusable and type-safe functions, classes, and interfaces by parameterizing types. Using generics allows you to write code that does not depend on specific types, enabling you to perform the same operations on various types.

Basics of Generics

Generics act like templates that accept types as arguments, allowing functions and classes to handle different types.

Generic Functions

The following is an example of a function with its argument types specified using generics.

1function identity<T>(value: T): T {
2    return value;
3}
4
5console.log(identity<number>(42));       // 42
6console.log(identity<string>("Hello"));  // Hello
  • T is a generic type argument that represents the types of the function's arguments and return value. The actual type is determined when the function is called.
  • By explicitly specifying <number> or <string>, you are specifying the type.

Generic types work without explicit specification because TypeScript performs type inference.

1function identity<T>(value: T): T {
2    return value;
3}
4
5console.log(identity(42));       // 42
6console.log(identity("Hello"));  // Hello
  • Even without explicitly specifying <number> or <string>, type inference takes place. identity(42) is inferred as number, and identity("Hello") is inferred as string.

Constraints on Generics

By putting constraints on generics, you can restrict them to accept only specific types.

1function loggingIdentity<T extends { length: number }>(arg: T): T {
2    console.log(arg.length);
3    return arg;
4}
5
6loggingIdentity("Hello");  // 5
7loggingIdentity([1, 2, 3]);  // 3
8
9// loggingIdentity(42);  // Error: number does not have a length property.
  • Specifying T extends { length: number } indicates that T must be a type with a length property. Therefore, types without a length property will not be accepted.

Combination with keyof

By combining generics with keyof, you can obtain property names in a type-safe manner.

1function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
2  return obj[key];
3}
4
5const person = { name: "Bob", age: 30 };
6const personName = getProperty(person, "name"); // string
7console.log(personName);
8
9// const error = getProperty(person, "unknown"); // Error
  • K is constrained by keyof T, indicating that only keys existing in T can be specified. Specifying a key that does not exist will result in a compilation error.

Generic Classes

Classes can also be defined using generics. Generic classes offer flexible types for properties and methods.

 1class Box<T> {
 2    private _value: T;
 3
 4    constructor(value: T) {
 5        this._value = value;
 6    }
 7
 8    public getValue(): T {
 9        return this._value;
10    }
11
12    public setValue(value: T): void {
13        this._value = value;
14    }
15}
16
17const numberBox = new Box<number>(100);
18console.log(numberBox.getValue());  // 100
19
20const stringBox = new Box<string>("Hello");
21console.log(stringBox.getValue());  // Hello
  • Box<T> declares the type T used within the class as a generic. This allows the same class to be reused for different types.

Generic Interfaces

Generics can also be used with interfaces.

 1interface Pair<T, U> {
 2    first: T;
 3    second: U;
 4}
 5
 6const numberStringPair: Pair<number, string> = { first: 1, second: "One" };
 7console.log(numberStringPair);  // { first: 1, second: 'One' }
 8
 9const booleanArrayPair: Pair<boolean, number[]> = { first: true, second: [1, 2, 3] };
10console.log(booleanArrayPair);  // { first: true, second: [ 1, 2, 3 ] }
  • By specifying two generic types with Pair<T, U>, you can define an object with a combination of different types.

Default Type Arguments

It is also possible to specify a default type for generic type arguments.

1function createArray<T = string>(length: number, value: T): T[] {
2    return Array(length).fill(value);
3}
4
5console.log(createArray(3, "a"));   // ['a', 'a', 'a']
6console.log(createArray(3, 100));   // [100, 100, 100]
  • We are setting the default type argument to string with <T = string>. If no type is specified explicitly, T will be of type string.

Generic Type Aliases

Generics can also be used as type aliases (type).

 1type Result<T> = {
 2    success: boolean;
 3    data: T;
 4};
 5
 6const successResult: Result<number> = { success: true, data: 42 };
 7const errorResult: Result<string> = { success: false, data: "Error occurred" };
 8
 9console.log(successResult);  // { success: true, data: 42 }
10console.log(errorResult);    // { success: false, data: 'Error occurred' }
  • Result<T> represents a result object containing data of type T. In this way, you can create flexible type aliases using generics.

Multiple Generic Types

By using multiple generic types, you can define even more versatile functions and classes.

1function merge<T, U>(obj1: T, obj2: U): T & U {
2    return { ...obj1, ...obj2 };
3}
4
5const person = { name: "Alice" };
6const job = { title: "Engineer" };
7
8const merged = merge(person, job);
9console.log(merged);  // { name: 'Alice', title: 'Engineer' }
  • The merge function takes two different types T and U and combines them to return a new object.

Summary

  • Generics enable reusable and type-safe code by treating types as parameters.
  • By using generics in functions, classes, and interfaces, you can write flexible logic that handles various types.
  • By adding constraints to type arguments or setting default type arguments, you can control the scope of generics.

By using generics, you can write type-independent, general-purpose code, making the most of TypeScript's powerful type system.

You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.

YouTube Video