Contents

Learn TypeScript: Generics

You may want to read this article in Bahasa Indonesia version: Mengenal TypeScript: Generics

Previously we have briefly discussed the use of Generics in Mengenal TypeScript: Interface & Type and Mengenal TypeScript: Type Assertions, Type Narrowing, & Function, but this time we will discuss in more detail about what Generics are in TypeScript. TypeScript Generics is a feature that allows us to create components that can work with multiple data types, not just one type. This article will explain the concept, syntax, and practical use of Generics in TypeScript.

What is Generics?

Generics allow us to create functions, classes, or interfaces that can work with a variety of data types while still maintaining type safety. With Generics, we can write code that is more flexible, reusable, and still type-safe.

Let’s start with a simple generic function example:

/** Generic function that returns the same argument */
function identity<T>(arg: T): T {
  return arg;
}

/** Using function with explicit type */
const output1 = identity<string>("Hello");  // output1 is of type string
const output2 = identity<number>(42);       // output2 is of type number

/** TypeScript can also infer the type automatically */
const output3 = identity("World");          // output3 is of type string
const output4 = identity(100);              // output4 is of type number

Pada contoh di atas, T adalah parameter tipe yang akan digantikan dengan tipe aktual saat fungsi dipanggil. Fungsi identity akan menerima argumen bertipe T dan mengembalikan nilai bertipe T juga.

Multiple Type Parameters

Generics can also use more than one type parameter:

/** Function with two type parameters */
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const result1 = pair<string, number>("key", 42);      // [string, number]
const result2 = pair<boolean, string[]>(true, ["a"]); // [boolean, string[]]

/** With type inference */
const result3 = pair("hello", 100);                   // [string, number]

Generic Constraints

Sometimes we want to limit the types that can be used with generics. This can be done with generic constraints:

/** Interface for constraint */
interface HasLength {
  length: number;
}

/** Function that only accepts types with length property */
function logLength<T extends HasLength>(arg: T): T {
  console.log(`Length: ${arg.length}`);
  return arg;
}

/** Valid because string has length property */
logLength("Hello"); // Length: 5

/** Valid because array has length property */
logLength([1, 2, 3]); // Length: 3

/** Valid because object has length property */
logLength({ length: 10 }); // Length: 10

/** Error: number does not have length property */
// logLength(42);

With the constraint T extends HasLength, we limit type T only to types that have a length property of type number.

Generic Interface and Type

Generics can also be used with interfaces and type aliases:

/** Generic interface */
interface Box<T> {
  value: T;
}

const stringBox: Box<string> = { value: "Hello" };
const numberBox: Box<number> = { value: 42 };

/** Generic type alias */
type Pair<T, U> = {
  first: T;
  second: U;
};

const keyValue: Pair<string, number> = { first: "key", second: 42 };

Generic Classes

We can also create generic classes:

/** Generic class */
class Container<T> {
  private item: T;

  constructor(item: T) {
    this.item = item;
  }

  getItem(): T {
    return this.item;
  }

  setItem(item: T): void {
    this.item = item;
  }
}

const numberContainer = new Container<number>(123);
console.log(numberContainer.getItem()); // 123

const stringContainer = new Container<string>("Hello");
console.log(stringContainer.getItem()); // "Hello"

Default Type Parameters

Generics can also have default values:

/** Interface with default type parameter */
interface ApiResponse<T = any> {
  data: T;
  status: number;
  message: string;
}

/** No need to specify type */
const response1: ApiResponse = {
  data: { name: "John" },
  status: 200,
  message: "Success",
};

/** Explicitly specifying type */
interface User {
  id: number;
  name: string;
}

const response2: ApiResponse<User> = {
  data: { id: 1, name: "John" },
  status: 200,
  message: "Success",
};

Generic Type Inference

TypeScript can infer generic types in many cases:

/** Generic function with inference */
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

/** TypeScript infers T as string */
const first = firstElement(["a", "b", "c"]); // first: string | undefined

/** TypeScript infers T as number */
const second = firstElement([1, 2, 3]); // second: number | undefined

/** TypeScript infers T as union type */
const mixed = firstElement([1, "a", true]); // mixed: string | number | boolean | undefined

Practical Use Cases

Here are some examples of the use of generics that are often encountered when we create applications using TypeScript.

/** Generic map function */
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
  return array.map(fn);
}

const numbers = [1, 2, 3, 4];
const doubled = map(numbers, (n) => n * 2);         // [2, 4, 6, 8]
const strings = map(numbers, (n) => n.toString());  // ["1", "2", "3", "4"]
const booleans = map(numbers, (n) => n % 2 === 0);  // [false, true, false, true]
interface Props<T> {
  data: T;
  render: (item: T) => string;
}

function Wrapper<T>(props: Props<T>): string {
  return props.render(props.data);
}

const numberWrapper = Wrapper({
  data: 42,
  render: (num) => `Number: ${num}`,
});

const userWrapper = Wrapper({
  data: { name: "John", age: 30 },
  render: (user) => `User: ${user.name}, Age: ${user.age}`,
});

console.log(numberWrapper); // "Number: 42"
console.log(userWrapper);   // "User: John, Age: 30"
/** Generic API function */
async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  return response.json() as Promise<T>;
}

interface User {
  id: number;
  name: string;
  email: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

/** Type-safe API calls */
async function loadData() {
  const user = await fetchData<User>("https://api.example.com/user/1");
  console.log(user.name); // TypeScript knows user has name property

  const product = await fetchData<Product>("https://api.example.com/product/1");
  console.log(product.price); // TypeScript knows product has price property
}

Keypoint Generics

Here are some key points about Generics:

  1. Reusability

    Generics allow us to write code that can be reused with different data types.

  2. Type Safety

    Despite being flexible, generics maintain type safety.

  3. Better Inference

    TypeScript can infer generic types in many cases.

  4. Constraints

    Generics make code more expressive and self-documenting.

  5. Readability

    Generics make code more expressive and self-documenting.

Conclusion

Generics are a powerful feature in TypeScript that allow us to create flexible yet type-safe components. By understanding and leveraging generics, we can write code that is more reusable, expressive, and safe. The functional programming approach becomes more powerful when combined with generics, allowing us to create utility functions that can work with various data types without sacrificing type safety.

Related Content