TypeScript类型系统全面解读
一、TypeScript 类型系统基础
1.1 基本类型
TypeScript 支持多种基本类型,这些类型是构建复杂类型的基石。
- 布尔类型(boolean):只有两个值
true
和false
。例如:
let isDone: boolean = false;
- 数字类型(number):在 TypeScript 中,所有数字都是浮点数。可以使用十进制、十六进制(以
0x
开头)或八进制(以0o
开头,ES2015 引入)来表示数字。
let myNumber: number = 42;
let hexNumber: number = 0xf00d;
let octalNumber: number = 0o755;
- 字符串类型(string):用于表示文本数据。可以使用单引号(
'
)、双引号("
)或模板字符串(${}
)。
let name: string = 'John';
let greeting: string = `Hello, ${name}`;
- 空值类型(void):通常用于表示函数没有返回值。
function logMessage(message: string): void {
console.log(message);
}
- Null 和 Undefined:
null
和undefined
在 TypeScript 中有自己的类型,分别为null
和undefined
。默认情况下,它们是所有类型的子类型。
let n: null = null;
let u: undefined = undefined;
- Never 类型:
never
类型表示那些永远不会有返回值的函数的返回类型,或者是那些根本不存在的值的类型。例如,一个抛出异常或无限循环的函数:
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
1.2 类型注解与类型推断
- 类型注解:是一种显式地告诉编译器某个变量或函数参数、返回值类型的方式。
let age: number;
age = 30;
- 类型推断:TypeScript 编译器会根据变量的初始化值自动推断其类型。例如:
let num = 10; // 这里 num 被推断为 number 类型
在函数中,返回值类型也可以通过类型推断得出:
function add(a, b) {
return a + b;
}
// 这里函数 add 的返回值被推断为 number 类型
二、复合类型
2.1 数组类型
- 明确元素类型的数组:可以使用
类型[]
的语法来定义数组。
let numbers: number[] = [1, 2, 3];
let strings: string[] = ['a', 'b', 'c'];
- 泛型数组类型:使用
Array<类型>
的形式,这与类型[]
等效。
let booleanArray: Array<boolean> = [true, false];
- 数组中元素类型不一致:可以使用联合类型来表示数组中元素类型不一致的情况。
let mixedArray: (number | string)[] = [1, 'two'];
2.2 元组类型
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
let tuple: [string, number] = ['test', 42];
访问元组元素时,TypeScript 会根据定义的类型进行检查:
let value: string = tuple[0]; // 正确
let wrongValue: number = tuple[0]; // 错误,类型不匹配
2.3 对象类型
- 对象字面量类型:用于定义对象的形状(shape),即对象包含哪些属性以及属性的类型。
let user: { name: string; age: number } = { name: 'Alice', age: 25 };
- 可选属性:在属性名后加上
?
表示该属性是可选的。
let userWithOptionalProp: { name: string; age?: number } = { name: 'Bob' };
- 只读属性:在属性名前加上
readonly
表示该属性只能在对象初始化时赋值。
let readonlyUser: { readonly name: string; age: number } = { name: 'Charlie', age: 30 };
// readonlyUser.name = 'New Name'; // 错误,不能重新赋值只读属性
三、函数类型
3.1 函数声明与类型
- 函数参数与返回值类型:在函数声明中明确参数和返回值的类型。
function addNumbers(a: number, b: number): number {
return a + b;
}
- 函数表达式类型:对于函数表达式,也需要明确类型。
let subtract: (a: number, b: number) => number = function (a, b) {
return a - b;
};
3.2 可选参数与默认参数
- 可选参数:在参数名后加上
?
表示该参数是可选的。
function greet(name: string, greeting?: string) {
if (greeting) {
return `${greeting}, ${name}`;
}
return `Hello, ${name}`;
}
greet('John');
greet('Jane', 'Hi');
- 默认参数:为参数提供默认值,具有默认值的参数也可以不传递。
function multiply(a: number, b: number = 1) {
return a * b;
}
multiply(5);
multiply(5, 3);
3.3 剩余参数
使用 ...
语法来表示剩余参数,它将所有剩余的参数收集到一个数组中。
function sum(...numbers: number[]): number {
return numbers.reduce((acc, num) => acc + num, 0);
}
sum(1, 2, 3);
四、类型别名与接口
4.1 类型别名
类型别名可以给一个类型起一个新名字,它可以是基本类型、复合类型或联合类型等。
type UserID = number;
let id: UserID = 123;
type StringOrNumber = string | number;
let value: StringOrNumber = 'test';
value = 42;
对于函数类型,也可以使用类型别名:
type MathOperation = (a: number, b: number) => number;
let add: MathOperation = function (a, b) {
return a + b;
};
4.2 接口
接口用于定义对象的形状,与类型别名类似,但在一些方面有所不同。
interface User {
name: string;
age: number;
}
let user: User = { name: 'David', age: 28 };
- 接口的继承:接口可以继承其他接口,以扩展其功能。
interface Admin extends User {
role: string;
}
let admin: Admin = { name: 'Eve', age: 35, role: 'admin' };
- 接口与类型别名的区别:
- 扩展方式:接口可以通过继承来扩展,类型别名可以通过联合类型来组合。例如:
interface A {
a: number;
}
interface B extends A {
b: string;
}
type C = { c: boolean };
type D = A & C;
- **声明合并**:接口支持声明合并,多次声明同一个接口会自动合并其成员,而类型别名不能声明合并。
interface Point {
x: number;
}
interface Point {
y: number;
}
let point: Point = { x: 1, y: 2 };
五、联合类型与交叉类型
5.1 联合类型
联合类型表示一个值可以是多种类型之一,使用 |
分隔不同的类型。
let value: string | number;
value = 'test';
value = 42;
在使用联合类型的值时,需要进行类型检查:
function printValue(value: string | number) {
if (typeof value ==='string') {
console.log(value.length);
} else {
console.log(value.toFixed(2));
}
}
5.2 交叉类型
交叉类型表示一个值同时具有多种类型的特性,使用 &
连接不同的类型。
interface A {
a: number;
}
interface B {
b: string;
}
let ab: A & B = { a: 1, b: 'test' };
交叉类型常用于组合多个接口的功能。例如,将一个具有 name
属性的接口和一个具有 age
属性的接口组合:
interface Nameable {
name: string;
}
interface Ageable {
age: number;
}
let person: Nameable & Ageable = { name: 'Frank', age: 32 };
六、类型断言
类型断言是一种告诉编译器“我知道自己在做什么”的方式,用于手动指定一个值的类型。有两种语法:
- 尖括号语法:
(<类型>值)
let someValue: any = 'this is a string';
let strLength: number = (<string>someValue).length;
- as 语法:
(值 as 类型)
let someOtherValue: any = 'this is another string';
let otherStrLength: number = (someOtherValue as string).length;
在 JSX 中,只能使用 as
语法进行类型断言。类型断言并不是类型转换,它只是告诉编译器以特定类型来处理值,并不会在运行时改变值的实际类型。
七、泛型
7.1 泛型函数
泛型函数允许我们创建可复用的函数,这些函数可以接受多种类型的参数,而不需要为每种类型都编写一个单独的函数。
function identity<T>(arg: T): T {
return arg;
}
let result1 = identity<number>(42);
let result2 = identity<string>('hello');
这里的 <T>
是类型参数,它可以在函数中代表任何类型。调用函数时,可以显式指定类型参数,也可以让 TypeScript 进行类型推断:
let inferredResult = identity(10); // 这里 T 被推断为 number 类型
7.2 泛型接口
可以定义泛型接口,用于描述泛型函数的形状或对象的泛型属性。
interface GenericIdentityFn {
<T>(arg: T): T;
}
let myIdentity: GenericIdentityFn = function <T>(arg: T): T {
return arg;
};
也可以将类型参数放在接口名称之后:
interface GenericInterface<T> {
value: T;
getValue(): T;
}
class GenericClass<T> implements GenericInterface<T> {
constructor(public value: T) {}
getValue(): T {
return this.value;
}
}
let instance = new GenericClass<number>(42);
7.3 泛型约束
有时我们需要对泛型类型进行一些约束,以确保它具有某些属性或方法。
interface Lengthwise {
length: number;
}
function printLength<T extends Lengthwise>(arg: T) {
console.log(arg.length);
}
printLength('hello');
printLength([1, 2, 3]);
// printLength(10); // 错误,number 类型没有 length 属性
通过 extends
关键字,我们约束了泛型 T
必须是具有 length
属性的类型。
八、条件类型
8.1 基本条件类型
条件类型基于一个条件来选择类型,使用 T extends U? X : Y
的语法,意思是如果 T
是 U
的子类型,则选择 X
类型,否则选择 Y
类型。
type IsString<T> = T extends string? true : false;
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false
8.2 映射类型
映射类型允许我们基于现有的类型创建新的类型,通过对现有类型的属性进行遍历和转换。
interface User {
name: string;
age: number;
}
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
let readonlyUser: ReadonlyUser = { name: 'Grace', age: 27 };
// readonlyUser.name = 'New Name'; // 错误,只读属性不能重新赋值
这里使用 keyof User
获取 User
接口的所有属性名,P in keyof User
遍历这些属性名,然后将每个属性变为只读。
8.3 索引类型
索引类型用于通过索引访问对象类型的属性类型。
interface Person {
name: string;
age: number;
}
type NameType = Person['name']; // string
type AgeType = Person['age']; // number
结合条件类型和索引类型,可以实现更复杂的类型操作。例如,获取对象中特定类型属性的类型:
interface AllTypes {
a: string;
b: number;
c: boolean;
}
type StringKeys<T> = {
[P in keyof T]: T[P] extends string? P : never;
}[keyof T];
type StringKey = StringKeys<AllTypes>; // 'a'
九、类型守卫与类型缩小
9.1 类型守卫函数
类型守卫是一个返回 boolean
的函数,用于在运行时检查一个值的类型。
function isString(value: any): value is string {
return typeof value ==='string';
}
function printValue(value: string | number) {
if (isString(value)) {
console.log(value.length);
} else {
console.log(value.toFixed(2));
}
}
这里的 isString
函数就是一个类型守卫,value is string
语法告诉 TypeScript,在函数返回 true
时,value
的类型为 string
。
9.2 typeof 类型守卫
typeof
操作符在 TypeScript 中也可以用作类型守卫。
function printValue2(value: string | number) {
if (typeof value ==='string') {
console.log(value.length);
} else {
console.log(value.toFixed(2));
}
}
通过 typeof
检查,TypeScript 能够在相应的代码块中缩小 value
的类型范围。
9.3 instanceof 类型守卫
instanceof
用于检查一个对象是否是某个类的实例,也可以作为类型守卫。
class Animal {}
class Dog extends Animal {}
function handleAnimal(animal: Animal) {
if (animal instanceof Dog) {
console.log('This is a dog');
} else {
console.log('This is some other animal');
}
}
在 if
块中,animal
的类型被缩小为 Dog
。
十、实用类型
10.1 Partial
Partial<T>
实用类型将类型 T
的所有属性变为可选。
interface User {
name: string;
age: number;
}
let partialUser: Partial<User> = {};
partialUser.name = 'Henry';
10.2 Required
Required<T>
与 Partial<T>
相反,它将类型 T
的所有可选属性变为必选。
interface OptionalUser {
name?: string;
age?: number;
}
let requiredUser: Required<OptionalUser> = { name: 'Ivy', age: 31 };
10.3 Readonly
Readonly<T>
将类型 T
的所有属性变为只读。
interface MutableUser {
name: string;
age: number;
}
let readonlyUser: Readonly<MutableUser> = { name: 'Leo', age: 29 };
// readonlyUser.name = 'New Name'; // 错误,只读属性不能重新赋值
10.4 Pick
Pick<T, K>
从类型 T
中选择属性集合 K
来创建一个新类型。
interface FullUser {
name: string;
age: number;
email: string;
}
type NameAndAge = Pick<FullUser, 'name' | 'age'>;
let nameAndAgeUser: NameAndAge = { name: 'Nina', age: 26 };
10.5 Omit
Omit<T, K>
从类型 T
中排除属性集合 K
来创建一个新类型。
interface AllInfo {
name: string;
age: number;
address: string;
}
type WithoutAddress = Omit<AllInfo, 'address'>;
let withoutAddressUser: WithoutAddress = { name: 'Oscar', age: 33 };
通过全面了解 TypeScript 的类型系统,开发者能够编写出更健壮、可维护的代码,充分发挥 TypeScript 在大型项目中的优势。无论是基础类型、复合类型,还是复杂的泛型、条件类型等,每个部分都紧密配合,为代码的类型安全提供了有力保障。