引言:为什么 TypeScript 成为前端开发必备技能?
今天我们来深入探讨 TypeScript 的高级类型系统。随着前端项目越来越复杂,TypeScript 已经从一个"可选项"变成了"必备技能"。掌握它的类型系统,能让你写出更健壮、更易维护的代码。
据调查,超过 80% 的前端项目正在使用 TypeScript,而掌握高级类型技巧的开发者在薪资上平均高出 20-30%。让我们一起来解锁这个强大的工具!
文章目录
一、TypeScript 基础语法
使用Javascript时经常会遇到这样一个问题:一个变量可能我们最初预期是 string类型,但在后续的代码中或函数执行过程中,该变量的类型变成了number,这就是我们所说的类型不安全。
Typescript 语法只是在 JavaScript 基础上增加了更多类型定义方面的内容,以此保证程序的类型安全。Typescript 首先要掌握的就是:类型定义。
1.1 基本类型注解
TypeScript 提供了丰富的类型注解,让代码更加自文档化。
// 基本类型
let isDone: boolean = false;
let count: number = 42;
let name: string = "TypeScript";
// 数组和元组
let list: number[] = [1, 2, 3];
let tuple: [string, number] = ["hello", 10];
// 枚举类型
enum Color { Red, Green, Blue }
let c: Color = Color.Green;
// 任意类型(谨慎使用)
let notSure: any = 4;
notSure = "maybe a string";
// 空值
let unusable: void = undefined;
let nullValue: null = null;
1.2 类型联合与交叉
类型联合用于指定一个值的类型可以是多个,我们可以把一个业务化的数据变量指定成既可以是 string 又可以是 number。
let myFavoriteNumber: string | number;
类型交叉是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
type NameProtocal = {name: string}
type PersonLikeProtocal = {age: number; say: () => void}
type Student = NameProtocal & PersonLikeProtocal
1.3 接口与类型别名
接口和类型别名是 TypeScript 的核心概念。
// 接口定义
interface User {
id: number;
name: string;
email?: string; // 可选属性
readonly createdAt: Date; // 只读属性
}
// 类型别名
type Point = {
x: number;
y: number;
};
// 函数类型
interface SearchFunc {
(source: string, subString: string): boolean;
}
// 可索引类型
interface StringArray {
[index: number]: string;
}
二、泛型:类型系统的强大武器
2.1 泛型基础
泛型(Generics)是创建可复用、类型安全的组件的重要特性。它允许我们编写可以处理多种数据类型的代码,而不需要为每种类型重复编写代码。即泛型让代码可以处理多种类型而不是单一类型。
核心思想:类型参数化
泛型就像是函数的类型参数,让你在定义函数、接口或类时暂不指定具体类型,而是在使用时再指定。
// 泛型函数
function identity<T>(arg: T): T {
return arg;
}
// 使用
let output = identity<string>("myString");
let output2 = identity(42); // 类型推断
// 泛型接口
interface GenericIdentityFn<T> {
(arg: T): T;
}
// 泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
2.2 泛型约束
有时候我们需要对泛型参数做一些限制。
// 基础约束
interface Lengthwise {
length: number;
}
// 使用 extends 约束
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 现在可以访问 .length 属性
return arg;
}
// 在泛型约束中使用类型参数(约束泛型为特定类型),使用 keyof 约束
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
// 练习
let x = { a: 1, b: 2, c: 3 };
getProperty(x, "a"); // 正确
getProperty(x, "m"); // 错误:'m' 不在 'a' | 'b' | 'c' 中
三、类型体操:高级类型技巧
3.1 条件类型
条件类型让类型系统具备"判断"能力。条件类型使用 extends 关键字来判断一个类型是否可分配给另一个类型。
// 基本语法
T extends U ? X : Y
// 基础条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// 条件类型与泛型结合
type TypeName<T> = T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
"object";
// 分布式条件类型
type ToArray<T> = T extends any ? T[] : never;
type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]
3.2 映射类型
映射类型允许基于现有类型通过转换来创建新类型。
// 核心语法
// K:类型变量,依次遍历联合类型中的每个成员
// Keys:通常是 keyof T 或字符串字面量联合类型
// Type:为新属性的类型,可以基于原始类型的属性类型 T[K] 进行计算
{ [K in Keys]: Type }
// 只读映射
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 可选映射
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 实战:深度只读
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? (T[P] extends Function ? T[P] : DeepReadonly<T[P]>): T[P];
};
// 使用
interface User {
profile: {
name: string;
age: number;
};
}
type ReadonlyUser = DeepReadonly<User>;
TypeScript 内置了几个常用的映射类型。
// Partial<T> - 所有属性变为可选
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Required<T> - 所有属性变为必需
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Readonly<T> - 所有属性变为只读
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Record<K, T> - 创建对象类型
type Record<K extends keyof any, T> = {
[P in K]: T;
};
3.3 模板字面量类型
TypeScript 4.1 引入了模板字面量类型,与映射类型结合非常强大。
// 基础用法
type World = "world";
type Greeting = `hello ${World}`; // "hello world"
// 联合类型扩展
type Color = "red" | "blue";
type Size = "small" | "large";
type Style = `${Color}-${Size}`; // "red-small" | "red-large" | "blue-small" | "blue-large"
// 实战:CSS 类名生成
type BEM<B extends string, E extends string[], M extends string[]> =
`${B}__${E[number]}` | `${B}--${M[number]}`;
type ClassNames = BEM<"button", ["icon", "text"], ["disabled", "active"]>;
// "button__icon" | "button__text" | "button--disabled" | "button--active"
// 模板自变量类型与映射类型结合
// CSS 属性映射
type CSSProperties = {
margin?: string;
padding?: string;
color?: string;
backgroundColor?: string;
};
// 转换为 CSS 变量格式
type CSSVariables = {
[K in keyof CSSProperties as `--${K}`]: CSSProperties[K];
}; // 结果:{ "--margin"?: string; "--padding"?: string; ... }
// 事件监听器映射
type EventMap = {
click: MouseEvent;
keydown: KeyboardEvent;
submit: Event;
};
type EventHandlersMap = {
[K in keyof EventMap as `on${Capitalize<K>}`]: (event: EventMap[K]) => void;
};
// { onClick: (event: MouseEvent) => void;
// onKeydown: (event: KeyboardEvent) => void; ... }
四、实战项目应用场景
4.1 Vue组件 Props 类型设计
<script setup lang="ts" generic="T">
import { defineProps } from 'vue'
interface Props<T> {
items: T[]
itemKey: keyof T
itemLabel: keyof T
}
const props = defineProps<Props<T>>()
</script>
<template>
<ul>
<li v-for="item in items" :key="item[itemKey]">
{{ item[itemLabel] }}
</li>
</ul>
</template>
4.2 API 响应类型安全
// 统一的 API 响应类型
interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
timestamp: number;
}
// 分页数据
interface PaginatedData<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
// 用户相关类型
interface User {
id: number;
name: string;
email: string;
createdAt: string;
}
// API 函数类型安全
class UserApi {
// 获取用户,结果为Promise<ApiResponse<PaginatedData<User>>
async getUsers(params: { page: number; pageSize: number }): Promise<ApiResponse<PaginatedData<User>>> {
// 实现
}
// 添加用户,结果为Promise<ApiResponse<User>类型
async createUser(user: Omit<User, 'id' | 'createdAt'>): Promise<ApiResponse<User>> {
// 实现
}
}
4.3 状态管理类型安全
在Vue 3中,Pinia是官方推荐的状态管理库,它完全支持TypeScript,并且提供了很好的类型推断。我们可以通过泛型来定义store,从而获得完全类型安全的state、getters和actions。
// 定义store的泛型参数,T表示列表元素的类型
interface GenericListState<T> {
items: T[]
}
// 定义一个泛型的store工厂函数
export const createListStore = <T>() => {
return defineStore({
id: 'listStore', // 注意:每个store的id必须是唯一的
state: (): GenericListState<T> => ({
items: []
}),
getters: {
// 泛型getter:返回第一个元素,类型为T | undefined
firstItem: (state): T | undefined => state.items[0],
// 返回元素数量
count: (state): number => state.items.length
},
actions: {
// 添加一个元素,参数类型为T
addItem(item: T) {
this.items.push(item)
},
// 根据索引移除元素
removeItem(index: number) {
if (index >= 0 && index < this.items.length) {
this.items.splice(index, 1)
}
},
// 更新元素,根据索引和新的元素
updateItem(index: number, item: T) {
if (index >= 0 && index < this.items.length) {
this.items[index] = item
}
}
}
})
}
// 使用工厂函数创建特定类型的store
interface User {
id: number,
name: string
}
// 创建用户列表store
const useUserListStore = createListStore<User>()
// 在Vue组件中,我们可以这样使用:
const userStore = useUserListStore()
userStore.addItem({ id: 1, name: 'Alice' }) // 类型检查:必须符合User接口
userStore.addItem({ id: 2, name: 'Bob' })
console.log(userStore.firstItem) // 类型为User | undefined
五、高频面试题解析
问题1: interface 和 type 的区别
题目:interface 和 type 有什么区别?什么时候用 interface,什么时候用 type?
参考答案:
语法差异:interface 使用 interface关键字,type 使用 type关键字
扩展方式:interface 使用 extends,type 使用 &
合并声明:interface 支持声明合并,type 不支持
使用场景:
interface:描述对象形状、类实现
type:联合类型、交叉类型、映射类型等复杂类型
// interface 声明合并
interface User { name: string; }
interface User { age: number; }
// 最终 User 包含 name 和 age
// type 不支持声明合并
type User = { name: string; };
type User = { age: number; }; // 错误:重复标识符
问题2: (泛型约束)实现一个getValue函数,安全地获取嵌套对象的属性值
// 基础泛型函数
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// 定义一个条件类型,它检查 K 是否是 T 的键
type Get<T, K> = K extends keyof T ? T[K] : never;
// 深层次的嵌套获取
function getValueAdvanced<T, P extends string>(obj: T, path: P): P extends `${infer K}.${infer R}` //infer来推断参数的类型
? Get<Get<T, K>, R> //条件检查路径是否正确
: Get<T, P> {
// 将路径分割成数组:'a.b.c' -> ['a', 'b', 'c']
const keys = path.split('.') as any[]
// 逐层访问对象
let result: any = obj;
for (const key of keys) {
if (result == null) {
return undefined as any; // 实际应该根据类型调整
}
result = result[key];
}
return result;
}
PS:infer主要用来表示待推断的函数参数。更多关于infer的使用,可参照infer
问题3: (类型体操)实现一个 DeepRequired工具类型,使所有层级的属性都变为必填
// 内置的Required<T> - 所有属性变为必需
type Required<T> = {
[P in keyof T]-?: T[P];
};
type DeepRequired<T> = T extends object ? {[P in keyof T]-?: (T[P] extends object ?DeepRequired<T[P]> : T[P])} : T
// 测试
interface User {
profile?: {
name?: string;
age?: number;
};
}
type RequiredUser = DeepRequired<User>;
// 等价于:
// {
// profile: {
// name: string;
// age: number;
// };
// }
六、总结
TypeScript 高级类型核心价值
类型安全:在编译期捕获错误,减少运行时错误
代码自文档化:类型系统即文档
重构信心:类型检查保证重构的正确性
✅ 推荐做法
· 从简单类型开始,逐步学习高级特性
· 在实际项目中实践类型设计
· 使用严格的 TypeScript 配置
❌ 避免做法
· 不要过度使用 any类型
· 避免过于复杂的类型体操
· 不要忽视类型错误警告
下期预告
下一篇我们将深入探讨 JavaScript 设计模式与架构模式,包括常见的设计模式实现、前端架构设计原则,以及如何在大型项目中应用这些模式。
如果觉得有帮助,请关注+点赞+收藏,这是对我最大的鼓励! 如有问题,请评论区留言
751

被折叠的 条评论
为什么被折叠?



