TypeScript复合类型的构建与使用
联合类型(Union Types)
在 TypeScript 中,联合类型是一种复合类型,它允许一个变量具有多种类型中的一种。联合类型使用竖线(|
)来分隔不同的类型。例如,假设我们有一个函数,它可以接受一个字符串或者一个数字作为参数,我们可以这样定义参数的类型:
function printValue(value: string | number) {
console.log(value);
}
printValue('Hello');
printValue(42);
在上面的代码中,printValue
函数的 value
参数可以是 string
类型或者 number
类型。当我们调用这个函数时,传入字符串或者数字都不会报错。
联合类型的类型检查
TypeScript 会对联合类型进行类型检查。当我们在使用联合类型的变量时,我们只能访问这些类型中共有的属性和方法。例如:
function printLength(value: string | number) {
// 下面这行代码会报错,因为 number 类型没有 length 属性
// console.log(value.length);
}
在 printLength
函数中,我们不能直接访问 value.length
,因为 number
类型没有 length
属性。要解决这个问题,我们可以使用类型守卫(Type Guards)。
使用类型守卫处理联合类型
类型守卫是一种运行时检查,它可以让我们在代码中缩小联合类型的范围。常见的类型守卫有 typeof
、instanceof
等。
使用 typeof
作为类型守卫处理 string | number
联合类型:
function printLength(value: string | number) {
if (typeof value ==='string') {
console.log(value.length);
} else {
console.log('It is a number, no length property.');
}
}
printLength('Hello');
printLength(42);
在上面的代码中,通过 typeof value ==='string'
这个类型守卫,我们在 if
代码块中可以确定 value
是 string
类型,因此可以安全地访问 length
属性。
交叉类型(Intersection Types)
交叉类型是将多个类型合并为一个类型。它包含了所有类型的特性。交叉类型使用 &
符号来定义。例如,如果我们有两个类型 A
和 B
,我们可以创建一个新的类型 C
,它同时拥有 A
和 B
的属性:
type A = { name: string };
type B = { age: number };
type C = A & B;
let person: C = { name: 'John', age: 30 };
在上面的代码中,C
类型是 A
和 B
的交叉类型,所以 person
对象必须同时具有 name
属性(类型为 string
)和 age
属性(类型为 number
)。
交叉类型的应用场景
交叉类型在很多场景下都非常有用,比如当我们需要一个对象既有某个接口的属性,又有其他额外属性时。假设我们有一个 Button
接口,定义了按钮的基本属性,然后我们想要创建一个 SubmitButton
类型,它不仅具有 Button
的属性,还具有 form
属性:
interface Button {
label: string;
onClick(): void;
}
interface Form {
form: string;
}
type SubmitButton = Button & Form;
let submitButton: SubmitButton = {
label: 'Submit',
onClick() {
console.log('Button clicked');
},
form: 'loginForm'
};
这样,SubmitButton
类型的对象就同时具备了 Button
和 Form
的属性。
类型别名与接口的复合类型构建
类型别名构建复合类型
类型别名可以用来创建联合类型和交叉类型。我们前面已经看到了使用类型别名创建交叉类型的例子,下面再来看一个使用类型别名创建更复杂联合类型的情况。假设我们有不同类型的用户,普通用户和管理员用户,我们可以这样定义:
type RegularUser = {
username: string;
password: string;
};
type AdminUser = {
username: string;
password: string;
isAdmin: boolean;
};
type User = RegularUser | AdminUser;
function displayUser(user: User) {
if ('isAdmin' in user) {
console.log(`${user.username} is an admin.`);
} else {
console.log(`${user.username} is a regular user.`);
}
}
let regular: RegularUser = { username: 'user1', password: 'pass1' };
let admin: AdminUser = { username: 'admin1', password: 'adminpass', isAdmin: true };
displayUser(regular);
displayUser(admin);
在上面的代码中,我们通过类型别名 User
创建了一个联合类型,它可以是 RegularUser
或者 AdminUser
。在 displayUser
函数中,我们使用 'isAdmin' in user
作为类型守卫来判断用户类型。
接口构建复合类型
接口也可以用于构建复合类型,特别是交叉类型。我们可以通过继承多个接口来创建一个具有多个接口属性的新接口。例如:
interface Shape {
color: string;
}
interface Square {
sideLength: number;
}
interface ColoredSquare extends Shape, Square { }
let mySquare: ColoredSquare = {
color: 'blue',
sideLength: 5
};
在这个例子中,ColoredSquare
接口继承了 Shape
和 Square
接口,所以 ColoredSquare
类型的对象必须同时具有 color
和 sideLength
属性。
函数类型的复合
联合函数类型
函数类型也可以组成联合类型。假设我们有两个不同参数类型的函数,我们可以将它们组成一个联合函数类型。例如:
type AddNumbers = (a: number, b: number) => number;
type ConcatenateStrings = (a: string, b: string) => string;
type MathOrStringOperation = AddNumbers | ConcatenateStrings;
function performOperation(operation: MathOrStringOperation) {
if (typeof (operation as AddNumbers).length === 'number') {
let result = (operation as AddNumbers)(5, 3);
console.log('Number operation result:', result);
} else {
let result = (operation as ConcatenateStrings)('Hello, ', 'world');
console.log('String operation result:', result);
}
}
let add: AddNumbers = (a, b) => a + b;
let concat: ConcatenateStrings = (a, b) => a + b;
performOperation(add);
performOperation(concat);
在上面的代码中,MathOrStringOperation
是一个联合函数类型,它可以是 AddNumbers
类型的函数(接受两个数字并返回一个数字)或者 ConcatenateStrings
类型的函数(接受两个字符串并返回一个字符串)。在 performOperation
函数中,我们通过类型断言和一些简单的类型检查来确定调用哪个函数。
交叉函数类型
交叉函数类型相对较少见,但在某些特定场景下也很有用。它表示一个函数需要同时满足多个函数类型的签名。例如:
interface LoggableFunction {
(message: string): void;
log(message: string): void;
}
let myFunction: LoggableFunction = function (message) {
console.log(message);
};
myFunction('Direct call');
myFunction.log('Call through log method');
在这个例子中,LoggableFunction
是一个交叉函数类型,它要求函数既可以像普通函数一样调用(接受一个字符串参数并打印),又要有一个 log
方法,该方法也接受一个字符串参数并打印。
数组与元组的复合类型
数组的联合类型
数组元素可以具有联合类型。例如,我们可以创建一个数组,它的元素可以是字符串或者数字:
let mixedArray: (string | number)[] = ['Hello', 42];
在上面的数组定义中,mixedArray
数组的元素可以是 string
类型或者 number
类型。
元组的联合类型
元组也可以与联合类型结合使用。元组是一种固定长度和固定类型顺序的数组。假设我们有一个函数,它可以返回不同类型的元组:
type Result1 = [string, number];
type Result2 = [boolean, string];
type CombinedResult = Result1 | Result2;
function getResult(): CombinedResult {
const random = Math.random();
if (random > 0.5) {
return ['Success', 42];
} else {
return [false, 'Failure'];
}
}
let result = getResult();
if (typeof result[0] ==='string') {
console.log(`Operation was successful. Value: ${result[1]}`);
} else {
console.log(`Operation failed. Reason: ${result[1]}`);
}
在上面的代码中,CombinedResult
是一个联合类型,它可以是 Result1
类型的元组(第一个元素是字符串,第二个元素是数字)或者 Result2
类型的元组(第一个元素是布尔值,第二个元素是字符串)。在 getResult
函数中,根据随机数返回不同类型的元组,然后在使用 result
时,通过类型检查来处理不同类型的元组。
数组与交叉类型
数组元素也可以是交叉类型。例如,假设我们有一个数组,它的元素需要同时具有 name
属性(字符串类型)和 age
属性(数字类型):
type Person = { name: string } & { age: number };
let people: Person[] = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 }
];
在这个例子中,people
数组的元素是 Person
类型,而 Person
是一个交叉类型,它要求元素同时具有 name
和 age
属性。
类型推断与复合类型
联合类型的类型推断
当 TypeScript 进行类型推断时,对于联合类型也有特定的规则。例如,当我们给一个变量赋值为联合类型的字面量时,TypeScript 会推断出这个变量的类型为联合类型。
let value = 'Hello' || 42;
// value 的类型被推断为 string | number
在上面的代码中,value
的类型被推断为 string | number
,因为 'Hello' || 42
这个表达式的结果可能是字符串(如果 'Hello'
为真)或者数字(如果 'Hello'
为假,这里 'Hello'
恒为真,但 TypeScript 从逻辑上推断其可能为两种类型之一)。
交叉类型的类型推断
对于交叉类型,TypeScript 会根据赋值的对象来推断类型。例如:
let obj = { name: 'John', age: 30 };
// obj 的类型被推断为 { name: string; age: number; }
let combined: { name: string } & { age: number } = obj;
在这个例子中,obj
的类型被推断为 { name: string; age: number; }
,然后我们可以将 obj
赋值给 combined
,因为 combined
的类型是 { name: string } & { age: number }
,obj
的类型符合这个交叉类型的要求。
泛型与复合类型
泛型联合类型
泛型可以与联合类型结合使用,增强代码的灵活性。例如,我们可以创建一个函数,它可以接受一个数组,数组元素可以是不同类型,但这些类型都有一个共同的方法。假设我们有一个 Printable
接口,定义了 print
方法:
interface Printable {
print(): void;
}
class Book implements Printable {
constructor(public title: string) { }
print() {
console.log(`Book: ${this.title}`);
}
}
class Magazine implements Printable {
constructor(public name: string) { }
print() {
console.log(`Magazine: ${this.name}`);
}
}
function printItems<T extends Printable>(items: T[]) {
items.forEach(item => item.print());
}
let books: Book[] = [new Book('TypeScript in Action'), new Book('Effective TypeScript')];
let magazines: Magazine[] = [new Magazine('Tech Monthly'), new Magazine('Code World')];
printItems(books);
printItems(magazines);
在上面的代码中,printItems
函数是一个泛型函数,它接受一个数组,数组元素类型 T
必须是 Printable
类型或者其子类型。这里 Book
和 Magazine
都实现了 Printable
接口,所以可以将 Book
数组和 Magazine
数组传递给 printItems
函数。
泛型交叉类型
泛型也可以与交叉类型结合。例如,我们可以定义一个泛型类型,它是两个类型的交叉类型:
type Combine<T, U> = T & U;
interface A {
a: string;
}
interface B {
b: number;
}
let combined: Combine<A, B> = { a: 'Hello', b: 42 };
在这个例子中,Combine
是一个泛型类型别名,它创建了两个类型 T
和 U
的交叉类型。我们通过 Combine<A, B>
创建了一个新类型,这个类型同时具有 A
和 B
的属性,然后 combined
对象符合这个交叉类型的要求。
复合类型在 React 中的应用
React 组件属性的联合类型
在 React 开发中,我们经常会使用联合类型来定义组件的属性。例如,假设我们有一个 Button
组件,它的 variant
属性可以是 'primary'
或者 'secondary'
:
import React from'react';
type ButtonVariant = 'primary' |'secondary';
interface ButtonProps {
label: string;
variant: ButtonVariant;
}
const Button: React.FC<ButtonProps> = ({ label, variant }) => {
return <button className={`button-${variant}`}>{label}</button>;
};
export default Button;
在上面的代码中,ButtonVariant
是一个联合类型,它限定了 Button
组件 variant
属性的取值范围。这样可以在开发过程中避免错误地传递其他值给 variant
属性。
React 组件属性的交叉类型
有时候,我们可能需要一个组件的属性同时满足多个接口的要求。例如,我们有一个 Input
组件,它既需要基本的 InputProps
接口的属性,又需要 WithErrorProps
接口的属性来处理错误显示:
import React from'react';
interface InputProps {
type: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
interface WithErrorProps {
error: string | null;
}
type InputWithErrorProps = InputProps & WithErrorProps;
const InputWithError: React.FC<InputWithErrorProps> = ({ type, value, onChange, error }) => {
return (
<div>
<input type={type} value={value} onChange={onChange} />
{error && <span style={{ color:'red' }}>{error}</span>}
</div>
);
};
export default InputWithError;
在这个例子中,InputWithErrorProps
是 InputProps
和 WithErrorProps
的交叉类型,InputWithError
组件的属性必须同时满足这两个接口的要求。这样可以更好地组织和管理组件的属性,提高代码的可维护性。
通过以上对 TypeScript 复合类型的构建与使用的详细介绍,希望你能对联合类型、交叉类型等复合类型在不同场景下的应用有更深入的理解,并能在实际开发中灵活运用它们来提高代码的健壮性和可维护性。无论是简单的变量定义,还是复杂的函数和组件开发,复合类型都能发挥重要作用。