Generics
In TypeScript, generics are a powerful tool that allow us to write reusable components capable of adapting to multiple types.
1. Generic Constraints
Generics can be constrained to a specific type or interface, ensuring that the types passed to the generic meet certain conditions. For example, if we want a function to only accept types with a length
property, we can do this:
interface Lengthwise {
length: number;
}
function printLength<T extends Lengthwise>(item: T): void {
console.log(item.length);
}
const stringLength = "hello";
printLength(stringLength); // OK, strings have a length property
const objectWithoutLength = { name: "World" };
printLength(objectWithoutLength); // Error, no length property
<T extends Lengthwise>
ensures that T
must have a length
property.
2. Type Inference with Generics
TypeScript allows automatic inference of generic types in certain cases, particularly during function calls. For example, using the infer
keyword, we can extract the return type from a function type:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function identity<T>(arg: T): T {
return arg;
}
type IdentityReturnType = ReturnType<typeof identity>; // IdentityReturnType is inferred as 'string'
Here, ReturnType
extracts the return type from the function type.
3. Multi-Parameter Generics
You can define types or functions that accept multiple generic parameters:
interface Pair<T, U> {
first: T;
second: U;
}
function createPair<T, U>(first: T, second: U): Pair<T, U> {
return { first, second };
}
const pair = createPair("Hello", 42); // pair has type Pair<string, number>
The createPair
function accepts two generic parameters T
and U
and returns an object of type Pair<T, U>
.
4. Generic Interfaces
Interfaces can also use generics:
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity; // myIdentity is a number-specific version of the identity function
Here, GenericIdentityFn
is a generic interface, and the identity
function is assigned to an instance of it, restricting it to handle only number
type parameters.
5. Generic Classes
Classes can also be defined as generics, allowing methods and properties to use different types:
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
setValue(newValue: T): void {
this.value = newValue;
}
}
const boxOfStrings = new Box<string>("Hello");
boxOfStrings.setValue("World"); // OK
boxOfStrings.setValue(123); // Error, incompatible types
The Box
class accepts a generic type T
, and the specific type is specified during instantiation.
Union Types
Union types in TypeScript allow combining multiple types into a single type, meaning a variable can be one of several types.
1. Type Guards and Type Assertions
When dealing with union types, you may need to determine the specific type of a variable. This can be achieved with type guards, which are functions or expressions that check a variable's properties to narrow its possible type range. For example:
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };
function getArea(shape: Shape): number {
if ('radius' in shape) {
// Type guard: shape is now { kind: 'circle'; radius: number }
return Math.PI * shape.radius ** 2;
} else {
// Type guard: shape is now { kind: 'square'; side: number }
return shape.side ** 2;
}
}
In this example, checking if shape
has a radius
property determines whether it’s a circle or a square.
2. Non-null Assertion Operator (!)
The non-null assertion operator !
informs the compiler that, even if a union type includes null
or undefined
, you are certain the value is neither. However, incorrect usage may lead to runtime errors:
function logValue(value: string | null | undefined): void {
if (value) {
console.log(value!.toUpperCase()); // Use ! for non-null assertion
} else {
console.log('Value is null or undefined');
}
}
Here, if value
is not null
or undefined
, we use !
to suppress potential null
or undefined
type warnings.
3. Distributive Type Operator (& and |)
When applying union or intersection types to a generic type, they are distributed across each instance of the generic. For example, with an array whose elements are a union type:
type NumbersOrStrings = number | string;
type ArrayWithMixedElements<T> = T[];
const mixedArray: ArrayWithMixedElements<NumbersOrStrings> = [1, "two", 3];
mixedArray
’s element type is NumbersOrStrings
, so it can contain number
or string
.
4. Pattern Matching
In destructuring assignments, function parameters, or type aliases, pattern matching can be used to handle values of union types:
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };
function handleShape(shape: Shape) {
switch (shape.kind) {
case 'circle':
const { radius } = shape;
// Now we know shape is { kind: 'circle'; radius: number }
break;
case 'square':
const { side } = shape;
// Now we know shape is { kind: 'square'; side: number }
break;
}
}
In this example, the switch
statement acts as a type guard, handling different shape types based on the kind
property.
Intersection Types
Intersection types in TypeScript allow combining multiple types into a new type that includes all properties and methods of the original types.
1. Combining Types
Intersection types use the &
operator to merge two or more types. For example, suppose we have two interfaces, Person
and Employee
. We can create a PersonAndEmployee
intersection type:
interface Person {
name: string;
age: number;
}
interface Employee {
id: number;
department: string;
}
type PersonAndEmployee = Person & Employee;
const person: PersonAndEmployee = {
name: 'Alice',
age: 30,
id: 123,
department: 'HR',
};
The person
variable must satisfy the requirements of both Person
and Employee
interfaces.
2. Intersection of Classes and Interfaces
Intersection types can also be applied between classes and interfaces, combining a class instance with the properties and methods of an interface:
class Animal {
name: string;
makeSound(): void {
console.log('Making sound...');
}
}
interface HasColor {
color: string;
}
class ColoredAnimal extends Animal implements HasColor {
color: string;
}
type ColoredAnimalIntersection = Animal & HasColor;
function describeAnimal(animal: ColoredAnimalIntersection) {
console.log(`The ${animal.name} is ${animal.color} and makes a sound.`);
animal.makeSound();
}
const coloredCat = new ColoredAnimal();
coloredCat.name = 'Kitty';
coloredCat.color = 'Gray';
describeAnimal(coloredCat);
The ColoredAnimalIntersection
type is both an instance of the Animal
class and possesses the color
property from the HasColor
interface.
3. Type Guards
Intersection types are useful in type guards, especially when determining a specific type within a union type. For example, you might have an object that could be one of two types, and you want to determine which it is at a specific moment:
interface Movable {
move(): void;
}
interface Static {
stay(): void;
}
type ObjectState = Movable & Static;
function isMovable(obj: ObjectState): obj is Movable {
return typeof obj.move === 'function';
}
const object: ObjectState = {
move: () => console.log('Moving...'),
stay: () => console.log('Staying...')
};
if (isMovable(object)) {
object.move(); // Type guard ensures the move method exists
} else {
object.stay();
}
The isMovable
function is a type guard that checks if the move
method exists, confirming that object
is of type Movable
.
📘 *Want to get more practical programming tutorials? *
👨💻 If you want to systematically learn front-end, back-end, algorithms, and architecture design, I continue to update content packages on Patreon
🎁 I have compiled a complete series of advanced programming collections on Patreon:
- Weekly updated technical tutorials and project practice
- High-quality programming course PDF downloads
- Front-end / Back-end / Full Stack / Architecture Learning Collection
- Subscriber Exclusive Communication Group
👉 Click to join and systematically improve development capabilities: Advanced Development Tutorial
🚀 Join my technical exchange group to get daily useful information:
Thank you for your support and attention ❤️
Top comments (0)