Union types and intersection types
Basic Concepts of Union Types and Intersection Types
In TypeScript, union types and intersection types are two important ways of combining types in the type system. Union types, denoted by |
, allow a value to be one of several types; intersection types, denoted by &
, merge multiple types into a single type. These two type operators provide flexible type composition capabilities, enabling more precise descriptions of complex data structures.
// Union type example
type StringOrNumber = string | number;
// Intersection type example
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
In-Depth Analysis of Union Types
Union types are most commonly used in real-world development to handle variables that may be of multiple types. When TypeScript encounters a union type, it requires that only members common to all types can be accessed unless type guards are used to narrow the type.
function printId(id: string | number) {
if (typeof id === 'string') {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(2));
}
}
Union types can also be combined with literal types to create more precise type definitions:
type Direction = 'up' | 'down' | 'left' | 'right';
function move(direction: Direction) {
// ...
}
Practical Applications of Intersection Types
The primary purpose of intersection types is to merge multiple types into a single type, where the new type includes all properties of the original types. This is particularly useful when combining multiple interfaces or type aliases.
interface Employee {
id: number;
name: string;
}
interface Manager {
department: string;
subordinates: Employee[];
}
type ManagerEmployee = Employee & Manager;
const me: ManagerEmployee = {
id: 1,
name: 'John',
department: 'Engineering',
subordinates: []
};
Intersection types excel when working with mixin patterns:
class Disposable {
dispose() {
console.log('Disposing...');
}
}
class Activatable {
activate() {
console.log('Activating...');
}
}
type SmartObject = Disposable & Activatable;
function createSmartObject(): SmartObject {
const result = {} as SmartObject;
Object.assign(result, new Disposable(), new Activatable());
return result;
}
Differences Between Union Types and Intersection Types
Although both are type composition operators, union types represent an "or" relationship, while intersection types represent an "and" relationship. Understanding their differences is crucial for correct usage.
type A = { a: number };
type B = { b: string };
// Union type: can be either A or B
type AOrB = A | B;
const aOrB1: AOrB = { a: 1 }; // valid
const aOrB2: AOrB = { b: 'hello' }; // valid
// Intersection type: must be both A and B
type AAndB = A & B;
const aAndB: AAndB = { a: 1, b: 'hello' }; // valid
Union and Intersection in Advanced Type Operations
Union types and intersection types can be combined with other TypeScript features to create a more powerful type system.
Distributive Property in Conditional Types
type Box<T> = { value: T };
type Unbox<T> = T extends Box<infer U> ? U : T;
type StringOrNumberBox = Box<string> | Box<number>;
type Unboxed = Unbox<StringOrNumberBox>; // string | number
Mapped Types with Union/Intersection
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = PartialBy<User, 'id' | 'email'>;
/*
Equivalent to:
{
name: string;
id?: number;
email?: string;
}
*/
Union and Intersection in Utility Types
TypeScript's built-in utility types heavily use union and intersection types:
// Implementation principle of Partial<T>
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Implementation principle of Required<T>
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Implementation principle of Readonly<T>
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Type Inference with Union and Intersection
TypeScript's type inference has specific behaviors when handling union and intersection types:
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const nums = [1, 2, 3];
const num = firstElement(nums); // number | undefined
const mixed = [1, 'two', true];
const first = firstElement(mixed); // number | string | boolean | undefined
Function Overloads and Union Types
Union types can simplify function overload syntax:
// Traditional overload syntax
function padLeft(value: string, padding: number): string;
function padLeft(value: string, padding: string): string;
function padLeft(value: string, padding: any): string {
// implementation
}
// Simplified with union type
function padLeft(value: string, padding: number | string): string {
if (typeof padding === 'number') {
return Array(padding + 1).join(' ') + value;
}
return padding + value;
}
Type Guards and Union Types
Type guards are essential tools for working with union types:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function getSmallPet(): Fish | Bird {
// ...
}
const pet = getSmallPet();
// Using 'in' operator type guard
if ('fly' in pet) {
pet.fly();
} else {
pet.swim();
}
// Using custom type predicate
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
Type Merging in Intersection Types
How intersection types handle properties with the same name:
type A = { x: number; y: string };
type B = { x: string; z: boolean };
type C = A & B;
/*
Type C is:
{
x: never; // number & string → no value can be both number and string
y: string;
z: boolean;
}
*/
Union Types in React Applications
React's prop type definitions frequently use union types:
type ButtonProps = {
size: 'small' | 'medium' | 'large';
variant: 'primary' | 'secondary' | 'tertiary';
disabled?: boolean;
onClick?: () => void;
};
const Button: React.FC<ButtonProps> = ({ size, variant, disabled, onClick }) => {
// component implementation
};
Extending Component Props with Intersection Types
In React higher-order components, intersection types are used to merge props:
interface WithLoadingProps {
loading: boolean;
}
function withLoading<P extends object>(Component: React.ComponentType<P>) {
return function WithLoading(props: P & WithLoadingProps) {
return props.loading ? <div>Loading...</div> : <Component {...props} />;
};
}
// Usage
const EnhancedButton = withLoading(Button);
Template Literal Types and Unions
Template literal types introduced in TypeScript 4.1 can be combined with union types:
type VerticalAlignment = 'top' | 'middle' | 'bottom';
type HorizontalAlignment = 'left' | 'center' | 'right';
type Alignment = `${VerticalAlignment}-${HorizontalAlignment}`;
// Result: "top-left" | "top-center" | "top-right" |
// "middle-left" | "middle-center" | "middle-right" |
// "bottom-left" | "bottom-center" | "bottom-right"
Index Access Types and Unions
Accessing index types through union types:
interface Person {
name: string;
age: number;
address: {
street: string;
city: string;
};
}
type PersonProperty = keyof Person; // "name" | "age" | "address"
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person: Person = { name: 'John', age: 30, address: { street: 'Main', city: 'NY' } };
const age = getProperty(person, 'age'); // number
Conditional Types and Union Distribution
Conditional types automatically distribute over union types:
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>; // string[] | number[]
// Equivalent to
type StrOrNumArray = ToArray<string> | ToArray<number>;
Intersection Types and Generic Constraints
Intersection types can enhance generic constraints:
function merge<T extends object, U extends object>(first: T, second: U): T & U {
return { ...first, ...second };
}
const merged = merge({ name: 'John' }, { age: 30 });
// { name: 'John', age: 30 }
Union Types in Redux Applications
Redux action types are typically defined using union types:
type Action =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; index: number }
| { type: 'SET_VISIBILITY_FILTER'; filter: string };
function todos(state = [], action: Action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { text: action.text, completed: false }];
case 'TOGGLE_TODO':
return state.map((todo, index) =>
index === action.index ? { ...todo, completed: !todo.completed } : todo
);
default:
return state;
}
}
Intersection Types and Function Overloads
Intersection types can merge multiple function types:
type Func1 = (a: number) => number;
type Func2 = (b: string) => string;
type CombinedFunc = Func1 & Func2;
// Implementation must handle all cases
const func: CombinedFunc = (arg: number | string) => {
if (typeof arg === 'number') {
return arg * 2;
} else {
return arg.toUpperCase();
}
};
Union Types and the never Type
The never type has special behavior in union types:
type T = string | never; // equivalent to string
type U = string & never; // equivalent to never
Intersection Types and Interface Inheritance
Intersection types can achieve effects similar to interface inheritance:
interface A {
a: number;
}
interface B extends A {
b: string;
}
// Similar effect using intersection type
type C = A & { b: string };
Union Types in API Response Handling
Union types can represent different response states when handling API responses:
type ApiResponse<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function handleResponse<T>(response: ApiResponse<T>) {
switch (response.status) {
case 'loading':
return 'Loading...';
case 'success':
return response.data;
case 'error':
return `Error: ${response.error.message}`;
}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn