DevZone Logo

Generics - The most intimidating TypeScript feature

FS

MD Fahid Sarker

Senior Software Engineer · June 28, 2024


-
-
Share
Bookmark

Mastering TypeScript Generics: 10 Essential Tips

Master Type Script Generics

Hello Wizards! Today we have a treat for you. We're going to delve into 10 tips to make you a master of TypeScript generics. By the end of this post, you'll understand a lot more about generics and be confident enough to use them in your code. If you don't know what generics are or how to use them, then you are missing out. They give you the power to create abstractions, write DRY (Don't Repeat Yourself) code, and minimize the need for excess type annotations. TypeScript without generics would be absolutely awful. Even if you've never written them yourself, you've probably used them thousands of times.

Let's get started!

1. What is a Generic?

Generics refer to several different patterns. The first pattern is demonstrated here with myGenericType. Initially, it has data set to any. We can pass a type to it, making it a type function that takes an argument.

Code.typescript
type MyGenericType<TData> = { data: TData; }; // Error: Generic type 'MyGenericType' requires one type argument. let example1: MyGenericType; let example2: MyGenericType<string> = { data: "First Name" }; let example3: MyGenericType<number> = { data: 42 };

This allows us to create type helpers and functions that can generate other types.

2. Generic Functions

You can create functions with a type helper mapped over the top. Consider this makeFetch function:

Code.typescript
async function makeFetch<TData>(url: string): Promise<TData> { const res = await fetch(url); const data: TData = await res.json(); return data; }

This function maps a type argument, making it a generic function. It returns a promise typed with TData.

3. Built-in Generics

Many generics exist in the language. For example, the Set class is a built-in generic:

Code.typescript
let mySet = new Set<number>(); mySet.add(1); // mySet.add("string"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

Generics are common in libraries like React, where hooks such as useState, useRef, and useReducer often require type arguments.

4. Inference with Generics

Generics can often infer types from runtime arguments.

Code.typescript
function addIdToObject<T extends { id?: string }>(obj: T): T & { id: string } { return { ...obj, id: "123", }; } let objWithId = addIdToObject({ name: "Alice" }); // objWithId: { name: 'Alice', id: '123' }

Here, TypeScript infers the type without explicit type arguments, making the code cleaner.

5. Combining Awaited and ReturnType

You can combine utility types like Awaited and ReturnType to create advanced helpers:

Code.typescript
type GetPromiseReturnType<T extends (...args: any) => any> = Awaited< ReturnType<T> >;

This helper type takes a function that returns a promise and infers the resolved type.

6. Constrained Generics

You can constrain generics to ensure valid inputs:

Code.typescript
function getKeyWithHighestValue<TObj extends Record<string, number>>( obj: TObj ) { // implementation } let result = getKeyWithHighestValue({ a: 1, b: 3, c: 2 }); // result: { key: "b", value: 3 }

Constrained generics validate inputs at the type level, reducing runtime errors.

7. Type Assertions with Generics

Sometimes you need to override TypeScript's inference using type assertions:

Code.typescript
function typedObjectKeys<TObj>(obj: TObj): (keyof TObj)[] { return Object.keys(obj) as (keyof TObj)[]; }

This ensures TypeScript respects your expected return type.

8. Multiple Generics

You can use multiple generics for more specific typing:

Code.typescript
function getValue<TObj, TKey extends keyof TObj>( obj: TObj, key: TKey ): TObj[TKey] { return obj[key]; } let value = getValue({ a: 1, b: "string", c: true }, "b"); // value: string

This enables advanced type inference across multiple parameters.

9. Default Type Parameters

You can set default type parameters:

Code.typescript
function createSet<T = string>(): Set<T> { return new Set<T>(); } let defaultSet = createSet(); // defaultSet: Set<string>

Default type parameters provide sensible defaults while allowing overrides.

10. Using Schemas for Type Verification

Using libraries like Zod for runtime verification:

Code.typescript
import { z, ZodSchema } from "zod"; function makeFetchWithSchema<TData>( url: string, schema: ZodSchema<TData> ): Promise<TData> { return fetch(url) .then((res) => res.json()) .then((data) => schema.parse(data)); } const schema = z.object({ firstName: z.string(), lastName: z.string(), }); makeFetchWithSchema("https://example.com", schema).then((data) => console.log(data) ); // data: { firstName: string, lastName: string }

Generics combined with schema validation ensure type safety at both compile time and runtime.


Until next time, keep coding and may the generics be with you!

Found the blog helpful? Consider sharing it with your friends.
Buy Me a Coffee

TypescriptGenericsTypeScript

Related Blogs

Blogs that you might find interesting based on this blog.