Advanced Typescript Generics In Practice
Published: Jun 20, 2023
Last updated: Jun 20, 2023
This post will is 6 of 20 for my series on intermediate-to-advance TypeScript tips.
All tips can be run on the TypeScript Playground.
Introduction
Generics are a tool that gives you the ability to create reusable and flexible types, functions, and classes in TypeScript. A generic type is a type that is connected with another type. It is like a placeholder for any kind of type. In this blog post, we will explore advanced features of TypeScript generics, with hands-on examples you can try out in the TypeScript Playground.
Generic Constraints
Generic constraints in TypeScript allow you to apply certain conditions on the types that are used in your generic functions or classes.
interface Lengthy { length: number; } function countElements<T extends Lengthy>(element: T): number { return element.length; } console.log(countElements("Hello, TypeScript!")); // outputs: 18 console.log(countElements([1, 2, 3, 4, 5])); // outputs: 5
In the above code, T extends Lengthy
is a generic constraint that requires the type T
to have a length
property. The countElements
function will now work with any object that has a length
property, such as arrays or strings.
See the example here.
Using keyof with Generics
The keyof
operator in TypeScript returns a type that represents all possible keys of an object. We can use it in combination with generics to ensure type safety when accessing object properties.
function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } let obj = { a: 1, b: 2, c: 3 }; console.log(getProperty(obj, "a")); // outputs: 1 console.log(getProperty(obj, "d")); // Error: Argument of type '"d"' is not assignable to parameter of type '"a" | "b" | "c"'.
In the above example, K extends keyof T
is a constraint that makes sure the key
exists in the object obj
. The TypeScript compiler checks this at compile time.
See the example here.
Generic Classes
In TypeScript, classes can also be generic. Here is an example of a generic Queue
class.
class Queue<T> { private data = [] as T[]; push(item: T) { this.data.push(item); } pop(): T | undefined { return this.data.shift(); } } const queue = new Queue<number>(); queue.push(0); queue.push(1); console.log(queue.pop()); // outputs: 0 console.log(queue.pop()); // outputs: 1 queue.push("2"); // TypeError: Argument of type 'string' is not assignable to parameter of type 'number'
In this case, T
is a placeholder for any type. We can create a queue of numbers, strings, or any other type. The push
and pop
methods work with that specific type, ensuring type safety.
See the example here.
Mapped Types with Generics
Mapped types can be combined with generics to create new types based on existing ones.
type ReadOnly<T> = { readonly [P in keyof T]: T[P]; }; const mutableObject = { prop1: "Hello", prop2: "TypeScript", }; const readOnlyObject: ReadOnly<typeof mutableObject> = mutableObject; console.log(readOnlyObject.prop1); // outputs: 'Hello' readOnlyObject.prop1 = "Hi"; // Error: Cannot assign to 'prop1' because it is a read-only property.
In the above example, ReadOnly<T>
is a mapped type that takes a type T
and creates a new type where all properties are readonly
.
Remember, TypeScript generics are all about reusability, flexibility, and maintaining type safety. With a good understanding of generics, you can write more dynamic and reusable code.
See the example here.
Summary
In this blog post, we explored some of the advanced features of TypeScript generics. Generics allow for the creation of flexible, reusable components that maintain type safety, enhancing the development experience.
- Generic Constraints enable you to enforce certain conditions on the types used in generic functions or classes.
- The keyof operator with Generics provides type safety when accessing object properties, ensuring that the properties you're trying to access do exist on the object.
- Generic Classes are about defining classes where certain properties or methods are of a generic type, creating reusable and flexible class structures.
- Mapped Types with Generics allow you to create new types based on old ones, offering a way to create more flexible and dynamic type definitions.
These advanced generics techniques empower you to harness the power of TypeScript to create robust and type-safe code. They may appear complex at first, but with practice, they will become a vital tool in your TypeScript arsenal. Keep exploring and happy coding!
Resources and further reading
Photo credit: jeremybishop
Advanced Typescript Generics In Practice
Introduction