TypeScript 基本类型与复合类型:深入理解类型系统
基本类型
在 TypeScript 中,基本类型是构成类型系统的基础单元。这些类型直接对应于 JavaScript 中的基本数据类型,但增加了类型检查的能力,让开发者在编写代码时能更明确数据的类型,提前发现潜在的错误。
布尔类型(boolean)
布尔类型用于表示逻辑上的真或假,只有两个取值:true
和 false
。在 JavaScript 中,布尔值也是广泛用于条件判断等场景,而在 TypeScript 里,我们可以明确地声明一个变量为布尔类型。
let isDone: boolean = false;
// 正确,isDone 是布尔类型,可以赋值 true 或 false
isDone = true;
// 错误,不能将字符串类型赋值给布尔类型
isDone = 'true';
在函数中,我们也可以指定参数和返回值的类型为布尔类型。
function isGreaterThanTen(num: number): boolean {
return num > 10;
}
let result = isGreaterThanTen(15);
// result 是布尔类型,因为 isGreaterThanTen 函数返回布尔值
数字类型(number)
TypeScript 中的数字类型和 JavaScript 一样,所有数字都是以 64 位浮点格式表示。无论是整数还是小数,都用 number
类型表示。
let age: number = 25;
let pi: number = 3.14159;
// 八进制字面量,TypeScript 2.4 及以后版本支持,需开启 --esModuleInterop 或 --allowSyntheticDefaultImports
let octalLiteral: number = 0o744;
// 十六进制字面量
let hexLiteral: number = 0xf00d;
// 二进制字面量
let binaryLiteral: number = 0b1010;
在函数参数和返回值中使用数字类型也很常见。
function addNumbers(a: number, b: number): number {
return a + b;
}
let sum = addNumbers(5, 3);
// sum 是数字类型,因为 addNumbers 函数返回数字值
字符串类型(string)
字符串类型用于表示文本数据,在 TypeScript 中通过 string
关键字声明。和 JavaScript 一样,字符串可以用单引号('
)、双引号("
)或模板字符串(```)表示。
let name: string = 'John Doe';
let greeting: string = "Hello, " + name;
// 模板字符串
let message: string = `Welcome, ${name}`;
当函数处理字符串相关逻辑时,我们需要准确指定参数和返回值的类型。
function getFullName(first: string, last: string): string {
return `${first} ${last}`;
}
let fullName = getFullName('Jane', 'Smith');
// fullName 是字符串类型,因为 getFullName 函数返回字符串值
空值类型(void)
void
类型通常用于表示没有返回值的函数。在 JavaScript 中,函数如果没有 return
语句,实际上返回的是 undefined
,而在 TypeScript 里,对于这种情况可以明确指定返回值类型为 void
。
function logMessage(message: string): void {
console.log(message);
// 这里没有 return 语句,返回值类型为 void
}
let result2 = logMessage('This is a log');
// result2 的类型是 void,不能对其进行其他操作
需要注意的是,虽然 void
类型表示没有返回值,但也可以声明一个 void
类型的变量,不过只能将 undefined
或 null
赋值给它(在严格模式下,只有 undefined
可以赋值给 void
类型变量)。
let myVoid: void;
myVoid = undefined;
// 在严格模式下,以下赋值会报错
// myVoid = null;
null 和 undefined
在 TypeScript 中,null
和 undefined
有自己的类型,分别为 null
和 undefined
。它们是所有类型的子类型,这意味着可以将 null
和 undefined
赋值给其他类型的变量,但在严格模式下(strictNullChecks
开启),这种赋值会受到限制。
let num: number | null | undefined;
num = 10;
num = null;
num = undefined;
// 严格模式下
let num2: number;
// num2 = null; 报错,不能将 null 赋值给 number 类型变量
// num2 = undefined; 报错,不能将 undefined 赋值给 number 类型变量
通常,当我们不确定一个值是否存在时,可以使用联合类型(后面会详细介绍联合类型),如 number | null
或 string | undefined
来表示可能为 null
或 undefined
的值。
任意类型(any)
any
类型表示可以是任意类型的值。当我们不确定一个变量的类型,或者希望它能接受任何类型的值时,可以使用 any
类型。这在处理动态数据,如来自用户输入或第三方库的不确定类型数据时很有用。
let data: any;
data = 10;
data = 'Hello';
data = true;
function printValue(value: any) {
console.log(value);
}
printValue(123);
printValue('abc');
然而,过度使用 any
类型会失去 TypeScript 类型检查的优势,因为它绕过了类型检查机制。所以,在能确定类型的情况下,应尽量避免使用 any
类型。
never 类型
never
类型表示那些永不存在的值的类型。通常出现在函数抛出异常或永远不会有返回值的情况下。
function throwError(message: string): never {
throw new Error(message);
}
// 以下代码会报错,因为 throwError 不会返回值,不能赋值给变量
let result3 = throwError('Something went wrong');
function infiniteLoop(): never {
while (true) {
// 无限循环,不会有返回值
}
}
never
类型是所有类型的子类型,但没有类型是 never
的子类型(除了 never
自身)。这意味着 never
类型的值可以赋值给任何类型的变量,但其他类型的值不能赋值给 never
类型变量。
枚举类型(enum)
枚举类型是 TypeScript 为 JavaScript 增加的一种数据类型,用于定义一组命名常量。它允许我们用更友好、易读的方式表示一组相关的值。
// 数字枚举
enum Direction {
Up = 1,
Down,
Left,
Right
}
let myDirection: Direction = Direction.Up;
// 字符串枚举
enum Status {
Success = 'success',
Failure = 'failure'
}
let operationStatus: Status = Status.Success;
在数字枚举中,如果没有显式指定值,枚举成员会从 0 开始自动递增。在上述 Direction
枚举中,Down
的值为 2,Left
的值为 3,Right
的值为 4。
枚举类型在代码中可以提高可读性和可维护性,特别是在需要表示固定集合的场景,如状态码、方向等。
复合类型
复合类型是由基本类型或其他复合类型组合而成的类型,它们为我们构建复杂的数据结构和逻辑提供了强大的能力。
数组类型
在 TypeScript 中,数组类型用于表示一组相同类型元素的集合。有两种常见的声明数组类型的方式。
方式一:类型 + 方括号
let numbers: number[] = [1, 2, 3, 4];
let names: string[] = ['Alice', 'Bob', 'Charlie'];
这里明确指定了数组元素的类型,只有符合该类型的元素才能放入数组中。
// 错误,不能将字符串放入数字数组
numbers.push('five');
方式二:使用泛型 Array<类型>
let ages: Array<number> = [20, 21, 22];
这两种方式本质上是等价的,只是语法形式略有不同。
当数组元素类型不确定,但又需要有一定约束时,可以使用联合类型。
let mixedArray: (number | string)[] = [1, 'two', 3, 'four'];
此外,TypeScript 还支持多维数组,例如二维数组可以这样声明:
let matrix: number[][] = [
[1, 2],
[3, 4]
];
元组类型
元组类型是一种特殊的数组类型,它允许我们定义一个固定长度且每个位置元素类型都明确的数组。与普通数组不同,元组的元素类型和数量在定义时就确定了。
let userInfo: [string, number] = ['John', 25];
// 正确,符合元组定义
let userInfo2: [number, string] = [25, 'John'];
// 错误,类型顺序不符合元组定义
访问元组元素时,TypeScript 会根据定义的类型进行检查。
let name: string = userInfo[0];
let age: number = userInfo[1];
// 错误,访问越界,元组只有两个元素
let invalidAccess: string = userInfo[2];
元组类型在需要表示一组相关但类型不同的数据时非常有用,比如函数返回多个不同类型的值。
function getUser(): [string, number] {
return ['Jane', 30];
}
let [userName, userAge] = getUser();
联合类型
联合类型允许一个变量具有多种类型中的一种。使用竖线(|
)来分隔不同的类型。
let value: string | number;
value = 'Hello';
value = 10;
// 错误,不能将布尔值赋值给联合类型(只允许字符串或数字)
value = true;
在函数参数中使用联合类型,可以让函数接受多种类型的参数。
function printValue2(value: string | number) {
if (typeof value === 'string') {
console.log(value.length);
} else {
console.log(value.toFixed(2));
}
}
printValue2('abc');
printValue2(123.45);
需要注意的是,当使用联合类型时,只能访问这些类型共有的属性和方法。例如,string
有 length
属性,number
没有,所以在上述 printValue2
函数中,需要先判断 value
的类型再访问 length
属性。
交叉类型
交叉类型是将多个类型合并为一个类型,它包含了所有类型的特性。使用 &
符号来表示交叉类型。
interface A {
a: string;
}
interface B {
b: number;
}
let ab: A & B = {
a: 'hello',
b: 10
};
在上述例子中,ab
变量的类型是 A & B
,它必须同时满足 A
接口和 B
接口的要求,即包含 a
属性(字符串类型)和 b
属性(数字类型)。
交叉类型常用于混合多个对象类型的特性,比如在 React 中,组件的属性可能需要混合多种不同的接口类型。
interface StyleProps {
color: string;
}
interface LayoutProps {
width: number;
}
interface ButtonProps extends StyleProps, LayoutProps {
label: string;
}
function Button({ color, width, label }: ButtonProps) {
// 组件逻辑
}
这里 ButtonProps
是 StyleProps
和 LayoutProps
的交叉类型,同时还添加了自己的 label
属性。
接口类型
接口是 TypeScript 中非常重要的概念,用于定义对象的形状(结构)。接口可以描述对象拥有哪些属性以及这些属性的类型。
interface User {
name: string;
age: number;
email?: string;
}
let user: User = {
name: 'Tom',
age: 28,
email: 'tom@example.com'
};
在上述 User
接口中,name
和 age
是必需属性,而 email
是可选属性,使用 ?
表示。如果对象缺少必需属性或属性类型不匹配,TypeScript 会报错。
// 错误,缺少 age 属性
let invalidUser: User = {
name: 'Jerry'
};
// 错误,age 属性类型不匹配
let invalidUser2: User = {
name: 'Jerry',
age: 'twenty'
};
接口还可以定义函数类型。
interface AddFunction {
(a: number, b: number): number;
}
let add: AddFunction = function (a, b) {
return a + b;
};
这里 AddFunction
接口定义了一个接受两个数字参数并返回一个数字的函数类型。
接口之间还可以继承,通过继承可以复用其他接口的属性和方法。
interface Employee extends User {
employeeId: string;
}
let employee: Employee = {
name: 'Sam',
age: 32,
email:'sam@example.com',
employeeId: '12345'
};
Employee
接口继承了 User
接口,所以它除了拥有自己的 employeeId
属性外,还必须包含 User
接口的所有属性。
类型别名
类型别名和接口类似,也用于给类型定义一个新的名字。但类型别名可以表示更复杂的类型,包括基本类型、联合类型、交叉类型等。
type StringOrNumber = string | number;
let data2: StringOrNumber = 'abc';
data2 = 123;
type Point = {
x: number;
y: number;
};
let point: Point = {
x: 10,
y: 20
};
类型别名和接口有一些区别。接口只能用于定义对象类型,而类型别名可以表示任何类型。并且,类型别名在使用 &
和 |
时可以更方便地组合复杂类型。
type ID = number | string;
interface UserInfo {
name: string;
}
type UserWithID = UserInfo & { id: ID };
let userWithID: UserWithID = {
name: 'Eve',
id: 1
};
另外,当使用相同名称定义接口和类型别名时,接口会被视为类型别名的扩展,而不是冲突。
interface Animal {
name: string;
}
type Animal = Animal & { age: number };
let animal: Animal = {
name: 'Dog',
age: 5
};
函数类型
在 TypeScript 中,函数也有自己的类型。函数类型包括参数列表和返回值类型。
function addNumbers2(a: number, b: number): number {
return a + b;
}
let addFunction: (a: number, b: number) => number = addNumbers2;
这里 (a: number, b: number) => number
就是函数类型,它表示接受两个数字参数并返回一个数字的函数。
当函数作为参数传递时,准确指定函数类型非常重要。
function operate(a: number, b: number, fn: (a: number, b: number) => number): number {
return fn(a, b);
}
let result4 = operate(5, 3, addNumbers2);
在 operate
函数中,fn
参数的类型是一个函数类型,它要求传入的函数必须接受两个数字参数并返回一个数字。
字面量类型
字面量类型是指具体的字面量值所代表的类型,如 1
、'hello'
、true
等。我们可以将变量声明为字面量类型,这使得变量只能被赋值为该字面量值。
let myNumber: 10 = 10;
// 错误,不能赋值为其他值
myNumber = 20;
let myString: 'hello' = 'hello';
// 错误,不能赋值为其他字符串
myString = 'world';
字面量类型常与联合类型一起使用,用于限制变量的取值范围。
let status: 'active' | 'inactive' = 'active';
// 只能赋值为 'active' 或 'inactive'
status = 'inactive';
在函数参数中使用字面量类型联合,可以确保函数只接受特定的值。
function setStatus(state: 'online' | 'offline') {
console.log(`Status set to ${state}`);
}
setStatus('online');
// 错误,不能传入其他值
setStatus('unknown');
通过深入理解 TypeScript 的基本类型和复合类型,开发者能够更精确地定义数据结构,利用类型系统的优势提高代码的可读性、可维护性以及减少运行时错误,从而编写出更健壮的前端代码。在实际项目中,合理运用这些类型,结合 TypeScript 的其他特性,能够显著提升开发效率和代码质量。