Introduction
TypeScript enhances JavaScript with types, and among its powerful features are generics and union types. This guide dives deep into the concepts and practical applications of these features to produce robust and reusable code.
Understanding Generics
Generics in TypeScript enable the creation of components that work with any data type while still providing compile-time type checking. Look at the following example:
function getArray<T>(items : T[] ) : T[] {
return new Array().concat(items);
}
let myNumArr = getArray<number>([10, 20, 30]);
let myStrArr = getArray<string>(['Hello', 'World']);
Here, <T>
is a type variable that can be replaced with any type (number, string, etc.) when the function is called.
Union Types
Union types are a way of declaring a variable that can hold values of two or more different types. It’s represented using the pipe symbol (|
). For instance:
function printId(id: number | string) {
console.log('Your ID is: ' + id);
}
printId(101); // OK
printId('202'); // OK
//printId({ myID: 22422 }); // Error
This function accepts both number and string as types for the id
parameter.
Combining Generics with Union Types
Generics and union types can be combined to provide even more flexibility in your functions:
function merge<T, U>(objA: T, objB: U): T & U {
return Object.assign(objA, objB);
}
let result = merge({ name: 'John' }, { age: 30 });
// result is of type { name: string; age: number; }
This example merges two objects into one with properties from both types. The result is an intersection type of the input types.
Generic Constraints and Union Types
Sometimes you may need to constrain the types you want to apply to generics, to ensure better type safety:
function logAndReturn<T extends { toString: () => string }>(elem: T): T {
console.log(elem.toString());
return elem;
}
logAndReturn(123); // Error
logAndReturn(new Date()); // OK
The <T extends { toString: () => string }>
ensures that T has a toString method.
Conditional Types and Union Types
Conditional types enable you to work with types based on conditions, refining the way you can combine generics and union types:
type NonNullable<T> = T extends null | undefined ? never : T;
function getValue<T>(value: T): NonNullable<T> {
if (value == null) {
throw new Error('Value must not be null or undefined');
}
return value as NonNullable<T>;
}
getValue('string'); // OK
// getValue(null); // Error
Utility Types with Generics
TypeScript provides several utility types to make working with generics more convenient. One of the most common is Partial<T>
, which makes all properties of T optional:
interface User {
name: string;
age: number;
}
type PartialUser = Partial<User>;
// You can now create a PartialUser with just one of the two properties defined
let user: PartialUser = { name: 'Alice' };
Advanced Generic Patterns
You can also create more advanced generic structures:
type LinkedList<T> = T & { next: LinkedList<T> } | null;
function insertAtBeginning<T>(head: LinkedList<T>, element: T): LinkedList<T> {
return { ...element, next: head };
}
This pattern allows creating a linked list with any type of elements.
Practical Use Cases
Combining generics and union types is not just for academic use cases, but they can be applied to practical scenarios. Imagine you are working with an API and you want to handle different response types:
type Response = UserResponse | ProductResponse | ErrorResponse;
function handleResponse(response: Response) {
if ('username' in response) {
// Handle UserResponse:
...
} else if ('productName' in response) {
// Handle ProductResponse:
...
} else {
// Handle ErrorResponse:
...
}
}
This structure enables the function to correctly handle different types of responses based on the presence of certain fields.
Summary
This guide outlined the power and flexibility of TypeScript’s generics and union types. By leveraging these features, you can build complex, high-quality type systems that promote code reuse and maintainability.