|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
TypeScript作为JavaScript的超集,通过添加静态类型系统,为开发者提供了强大的工具来编写更安全、更可维护的代码。在大型应用程序开发中,类型安全不仅能减少运行时错误,还能提高代码的可读性和可维护性。本文将深入探讨TypeScript的类型系统,从基础概念到高级技巧,帮助你掌握编写无bug应用程序的技能。
TypeScript基础类型系统
基本类型
TypeScript提供了一系列基本类型,这些是构建复杂类型系统的基础:
- // 字符串类型
- let name: string = "Alice";
- // 数字类型
- let age: number = 30;
- // 布尔类型
- let isActive: boolean = true;
- // null和undefined
- let nullValue: null = null;
- let undefinedValue: undefined = undefined;
- // symbol类型
- let id: symbol = Symbol("id");
- // bigint类型
- let bigNumber: bigint = 9007199254740991n;
复制代码
数组和元组
数组是相同类型元素的集合,而元组则是固定长度、每个元素类型已知的数组:
- // 数组类型
- let numbers: number[] = [1, 2, 3, 4, 5];
- // 或者使用泛型语法
- let strings: Array<string> = ["hello", "world"];
- // 元组类型 - 固定长度和类型的数组
- let tuple: [string, number] = ["Alice", 30];
- // 可以添加命名元组提高可读性
- let user: [name: string, age: number] = ["Bob", 25];
复制代码
枚举
枚举允许我们定义一组命名常量:
- // 数字枚举
- enum Direction {
- Up, // 0
- Down, // 1
- Left, // 2
- Right // 3
- }
- // 字符串枚举
- enum Status {
- Success = "SUCCESS",
- Error = "ERROR",
- Pending = "PENDING"
- }
- // 使用枚举
- let currentDirection: Direction = Direction.Up;
- let currentStatus: Status = Status.Success;
复制代码
any和unknown的区别
any类型会绕过类型检查,而unknown类型更安全,要求在使用前进行类型检查:
- // any类型 - 绕过所有类型检查
- let data: any = "hello";
- data = 42;
- data.toUpperCase(); // 运行时可能出错,但TypeScript不报错
- // unknown类型 - 更安全的替代方案
- let safeData: unknown = "hello";
- safeData = 42;
- // 使用unknown前必须进行类型检查
- if (typeof safeData === "string") {
- console.log(safeData.toUpperCase()); // 安全
- }
- // 或者使用类型断言
- console.log((safeData as string).toUpperCase()); // 有风险,除非确定类型
复制代码
void和never类型
void表示函数没有返回值,而never表示永远不会返回的函数:
- // void类型 - 函数没有返回值
- function logMessage(message: string): void {
- console.log(message);
- // 没有return语句或返回undefined
- }
- // never类型 - 永远不会返回的函数
- function throwError(message: string): never {
- throw new Error(message);
- // 函数执行到这里会抛出错误,永远不会返回
- }
- function infiniteLoop(): never {
- while (true) {
- // 无限循环,永远不会返回
- }
- }
复制代码
接口和类型别名
接口定义和使用
接口是TypeScript中定义对象结构的主要方式:
- // 基本接口
- interface User {
- id: number;
- name: string;
- email: string;
- }
- // 使用接口
- function greetUser(user: User) {
- return `Hello, ${user.name}!`;
- }
- const user: User = {
- id: 1,
- name: "Alice",
- email: "alice@example.com"
- };
- console.log(greetUser(user));
复制代码
可选属性和只读属性
接口可以定义可选属性和只读属性:
- interface Product {
- readonly id: number; // 只读属性,创建后不能修改
- name: string;
- price?: number; // 可选属性,使用?标记
- description?: string; // 可选属性
- }
- const product: Product = {
- id: 1001,
- name: "Laptop"
- };
- // 以下代码会报错,因为id是只读的
- // product.id = 1002; // Error: Cannot assign to 'id' because it is read-only
- // 可以添加可选属性
- product.price = 999.99;
复制代码
索引签名
索引签名允许定义可以使用不同键访问的对象类型:
- // 字符串索引签名
- interface StringDictionary {
- [key: string]: string;
- }
- const capitals: StringDictionary = {
- USA: "Washington D.C.",
- UK: "London",
- Japan: "Tokyo"
- };
- // 数字索引签名
- interface NumberArray {
- [index: number]: string;
- }
- const names: NumberArray = ["Alice", "Bob", "Charlie"];
- console.log(names[0]); // "Alice"
- // 混合索引签名
- interface ExtendedDictionary {
- [key: string]: string | number;
- length: number; // 可以添加特定属性
- }
复制代码
类型别名
类型别名为类型提供新名称,可以用于任何类型:
- // 基本类型别名
- type Age = number;
- // 对象类型别名
- type Person = {
- name: string;
- age: Age;
- email?: string;
- };
- // 联合类型别名
- type ID = string | number;
- // 函数类型别名
- type SearchFunc = (source: string, subString: string) => boolean;
- // 使用类型别名
- const person: Person = {
- name: "John",
- age: 30
- };
- const getUserId = (): ID => "123-45-6789";
复制代码
接口vs类型别名
接口和类型别名有相似之处,但也有一些关键区别:
- // 接口可以扩展
- interface Animal {
- name: string;
- }
- interface Dog extends Animal {
- breed: string;
- }
- // 类型别名可以使用交叉类型模拟扩展
- type AnimalType = {
- name: string;
- };
- type DogType = AnimalType & {
- breed: string;
- };
- // 接口可以合并声明
- interface Box {
- height: number;
- }
- interface Box {
- width: number;
- }
- // 现在Box包含height和width
- const box: Box = { height: 5, width: 6 };
- // 类型别名不能合并,会报错
- /*
- type BoxType = {
- height: number;
- }
- type BoxType = { // Error: Duplicate identifier 'BoxType'
- width: number;
- }
- */
复制代码
函数类型安全
函数参数和返回值类型
为函数参数和返回值添加类型可以确保函数被正确使用:
- // 基本函数类型
- function add(a: number, b: number): number {
- return a + b;
- }
- // 函数表达式
- const multiply: (a: number, b: number) => number = function(a, b) {
- return a * b;
- };
- // 箭头函数
- const divide = (a: number, b: number): number => a / b;
- // 使用函数类型别名
- type MathOperation = (x: number, y: number) => number;
- const subtract: MathOperation = (x, y) => x - y;
复制代码
可选参数和默认参数
TypeScript支持可选参数和默认参数:
- // 可选参数
- function buildName(firstName: string, lastName?: string): string {
- if (lastName) {
- return `${firstName} ${lastName}`;
- }
- return firstName;
- }
- console.log(buildName("John")); // "John"
- console.log(buildName("John", "Doe")); // "John Doe"
- // 默认参数
- function greet(name: string = "Guest"): string {
- return `Hello, ${name}!`;
- }
- console.log(greet()); // "Hello, Guest!"
- console.log(greet("Alice")); // "Hello, Alice!"
复制代码
剩余参数
剩余参数允许函数接受任意数量的参数:
- // 剩余参数必须是数组类型
- function sum(...numbers: number[]): number {
- return numbers.reduce((total, current) => total + current, 0);
- }
- console.log(sum(1, 2, 3)); // 6
- console.log(sum(10, 20, 30, 40)); // 100
- // 带有必需参数和剩余参数
- function join(separator: string, ...strings: string[]): string {
- return strings.join(separator);
- }
- console.log(join(", ", "apple", "banana", "cherry")); // "apple, banana, cherry"
复制代码
函数重载
函数重载允许一个函数有多个调用签名:
- // 函数重载声明
- function makeDate(timestamp: number): Date;
- function makeDate(m: number, d: number, y: number): Date;
- function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
- if (d !== undefined && y !== undefined) {
- return new Date(y, mOrTimestamp, d);
- } else {
- return new Date(mOrTimestamp);
- }
- }
- const d1 = makeDate(12345678); // 使用第一个重载
- const d2 = makeDate(5, 6, 2023); // 使用第二个重载
- // const d3 = makeDate(5, 6); // 错误,没有匹配的重载
复制代码
泛型函数
泛型函数可以处理多种类型,同时保持类型安全:
- // 基本泛型函数
- function identity<T>(arg: T): T {
- return arg;
- }
- // 使用泛型函数
- let output = identity<string>("hello");
- let output2 = identity(42); // 类型推断为number
- // 多个类型参数
- function pair<T, U>(first: T, second: U): [T, U] {
- return [first, second];
- }
- const p = pair<string, number>("hello", 42);
- // 泛型约束
- function loggingIdentity<T extends { length: number }>(arg: T): T {
- console.log(arg.length); // 现在我们可以访问length属性
- return arg;
- }
- loggingIdentity("hello"); // 字符串有length属性
- loggingIdentity([1, 2, 3]); // 数组有length属性
- // loggingIdentity(42); // 错误,数字没有length属性
复制代码
类和泛型
类的基本类型注解
TypeScript为类添加了类型注解,使类更加类型安全:
- class Person {
- // 属性类型注解
- name: string;
- age: number;
-
- // 构造函数参数类型注解
- constructor(name: string, age: number) {
- this.name = name;
- this.age = age;
- }
-
- // 方法返回类型注解
- greet(): string {
- return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
- }
- }
- const person = new Person("Alice", 30);
- console.log(person.greet());
复制代码
访问修饰符
TypeScript支持访问修饰符来控制类成员的可访问性:
- class Employee {
- // public: 任何地方都可以访问
- public name: string;
-
- // private: 只能在类内部访问
- private id: number;
-
- // protected: 只能在类及其子类中访问
- protected department: string;
-
- // readonly: 只读属性
- readonly company: string = "ABC Corp";
-
- constructor(name: string, id: number, department: string) {
- this.name = name;
- this.id = id;
- this.department = department;
- }
-
- public getInfo(): string {
- return `${this.name} (ID: ${this.id}) works in ${this.department}`;
- }
-
- protected getDepartment(): string {
- return this.department;
- }
- }
- class Manager extends Employee {
- private teamSize: number;
-
- constructor(name: string, id: number, department: string, teamSize: number) {
- super(name, id, department);
- this.teamSize = teamSize;
- }
-
- public getManagerInfo(): string {
- // 可以访问父类的protected成员
- const dept = this.getDepartment();
- return `${this.name} manages ${this.teamSize} people in ${dept}`;
- }
- }
- const emp = new Employee("John", 123, "IT");
- console.log(emp.name); // 可以访问public成员
- // console.log(emp.id); // 错误,不能访问private成员
- // console.log(emp.department); // 错误,不能访问protected成员
- const manager = new Manager("Alice", 456, "Finance", 10);
- console.log(manager.getManagerInfo());
复制代码
抽象类和接口实现
抽象类和接口可以帮助定义类的结构:
- // 抽象类
- abstract class Animal {
- abstract makeSound(): void; // 必须在派生类中实现
-
- move(): void {
- console.log("Moving along!");
- }
- }
- // 接口
- interface Flyable {
- fly(): void;
- altitude: number;
- }
- // 实现抽象类和接口
- class Bird extends Animal implements Flyable {
- altitude: number;
-
- constructor(altitude: number) {
- super();
- this.altitude = altitude;
- }
-
- makeSound(): void {
- console.log("Tweet tweet!");
- }
-
- fly(): void {
- console.log(`Flying at ${this.altitude} meters`);
- }
- }
- const bird = new Bird(100);
- bird.makeSound(); // "Tweet tweet!"
- bird.move(); // "Moving along!"
- bird.fly(); // "Flying at 100 meters"
复制代码
泛型类
类也可以使用泛型来增加灵活性:
- class Box<T> {
- private content: T;
-
- constructor(initialContent: T) {
- this.content = initialContent;
- }
-
- getContent(): T {
- return this.content;
- }
-
- setContent(newContent: T): void {
- this.content = newContent;
- }
- }
- // 使用泛型类
- const stringBox = new Box<string>("Hello");
- console.log(stringBox.getContent()); // "Hello"
- stringBox.setContent("World");
- const numberBox = new Box<number>(42);
- console.log(numberBox.getContent()); // 42
- numberBox.setContent(100);
- // 类型推断
- const booleanBox = new Box(true);
- console.log(booleanBox.getContent()); // true
复制代码
泛型约束
泛型约束可以限制泛型参数的类型:
- // 泛型约束
- interface Lengthy {
- length: number;
- }
- class Collection<T extends Lengthy> {
- private items: T[] = [];
-
- addItem(item: T): void {
- this.items.push(item);
- }
-
- getFirstLongerThan(length: number): T | undefined {
- return this.items.find(item => item.length > length);
- }
-
- getAverageLength(): number {
- const totalLength = this.items.reduce((sum, item) => sum + item.length, 0);
- return this.items.length ? totalLength / this.items.length : 0;
- }
- }
- // 使用泛型约束类
- const stringCollection = new Collection<string>();
- stringCollection.addItem("Hello");
- stringCollection.addItem("World");
- stringCollection.addItem("TypeScript");
- console.log(stringCollection.getFirstLongerThan(5)); // "TypeScript"
- console.log(stringCollection.getAverageLength()); // 5.666...
- const arrayCollection = new Collection<number[]>();
- arrayCollection.addItem([1, 2, 3]);
- arrayCollection.addItem([4, 5]);
- console.log(arrayCollection.getFirstLongerThan(2)); // [1, 2, 3]
- console.log(arrayCollection.getAverageLength()); // 2.5
复制代码
高级类型技巧
联合类型和交叉类型
联合类型允许一个值可以是多种类型之一,交叉类型则组合多个类型:
- // 联合类型
- function formatValue(value: string | number): string {
- if (typeof value === "string") {
- return value.toUpperCase();
- } else {
- return value.toFixed(2);
- }
- }
- console.log(formatValue("hello")); // "HELLO"
- console.log(formatValue(42.5678)); // "42.57"
- // 交叉类型
- interface BusinessPartner {
- name: string;
- credit: number;
- }
- interface Identity {
- id: number;
- name: string;
- }
- type Employee = BusinessPartner & Identity;
- const employee: Employee = {
- id: 123,
- name: "John Doe",
- credit: 7500
- };
- // 类型收窄
- function printValue(value: string | number) {
- if (typeof value === "string") {
- // 在这个块中,TypeScript知道value是string类型
- console.log(value.toUpperCase());
- } else {
- // 在这个块中,TypeScript知道value是number类型
- console.log(value.toFixed(2));
- }
- }
复制代码
类型推断
TypeScript能够根据上下文自动推断类型:
- // 变量类型推断
- let message = "Hello"; // 推断为string类型
- message = 42; // 错误:不能将number类型分配给string类型
- // 函数返回类型推断
- function add(a: number, b: number) {
- return a + b; // 推断返回类型为number
- }
- // 最佳通用类型推断
- let arr = [0, 1, null]; // 推断为(number | null)[]
复制代码
映射类型
映射类型基于现有类型创建新类型:
- interface Person {
- name: string;
- age: number;
- address: string;
- }
- // 创建所有属性可选的映射类型
- type PartialPerson = {
- [K in keyof Person]?: Person[K];
- };
- // 创建所有属性只读的映射类型
- type ReadonlyPerson = {
- readonly [K in keyof Person]: Person[K];
- };
- // 使用映射类型
- const partialPerson: PartialPerson = {
- name: "John"
- // age和address是可选的
- };
- const readonlyPerson: ReadonlyPerson = {
- name: "Alice",
- age: 30,
- address: "123 Main St"
- };
- // readonlyPerson.name = "Bob"; // 错误,不能修改只读属性
复制代码
条件类型
条件类型根据条件选择一种类型:
- // 基本条件类型
- type NonNullable<T> = T extends null | undefined ? never : T;
- // 使用条件类型
- type StringOrNumber = string | number;
- type OnlyString = NonNullable<StringOrNumber>; // string | number
- // 条件类型与映射类型结合
- type ExtractProperties<T, U> = {
- [K in keyof T]: T[K] extends U ? K : never;
- }[keyof T];
- interface Product {
- id: number;
- name: string;
- price: number;
- inStock: boolean;
- }
- // 提取所有number类型的属性名
- type NumberProperties = ExtractProperties<Product, number>; // "id" | "price"
复制代码
工具类型
TypeScript提供了一些内置的工具类型,方便常见的类型转换:
- // Partial - 将所有属性变为可选
- interface Todo {
- title: string;
- description: string;
- completed: boolean;
- }
- type PartialTodo = Partial<Todo>;
- // 相当于:
- // type PartialTodo = {
- // title?: string;
- // description?: string;
- // completed?: boolean;
- // }
- // Required - 将所有属性变为必需
- type RequiredTodo = Required<PartialTodo>;
- // 相当于原来的Todo
- // Pick - 选择一组属性
- type TodoPreview = Pick<Todo, "title" | "completed">;
- // 相当于:
- // type TodoPreview = {
- // title: string;
- // completed: boolean;
- // }
- // Omit - 排除一组属性
- type TodoWithoutDescription = Omit<Todo, "description">;
- // 相当于TodoPreview
- // Record - 创建对象类型
- type PageInfo = {
- title: string;
- };
- type Page = "home" | "about" | "contact";
- type Pages = Record<Page, PageInfo>;
- // 相当于:
- // type Pages = {
- // home: PageInfo;
- // about: PageInfo;
- // contact: PageInfo;
- // }
- // Exclude - 排除类型
- type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
- // Extract - 提取类型
- type T1 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
- // ReturnType - 获取函数返回类型
- function createUserId(name: string, id: number): string {
- return `${name}-${id}`;
- }
- type UserId = ReturnType<typeof createUserId>; // string
复制代码
类型守卫和类型断言
typeof和instanceof类型守卫
类型守卫帮助TypeScript在特定上下文中收窄类型:
- // typeof类型守卫
- function processValue(value: string | number) {
- if (typeof value === "string") {
- // 在这个块中,value被收窄为string类型
- console.log(value.toUpperCase());
- } else {
- // 在这个块中,value被收窄为number类型
- console.log(value.toFixed(2));
- }
- }
- // instanceof类型守卫
- class Cat {
- meow() {}
- }
- class Dog {
- bark() {}
- }
- function processAnimal(animal: Cat | Dog) {
- if (animal instanceof Cat) {
- // 在这个块中,animal被收窄为Cat类型
- animal.meow();
- } else {
- // 在这个块中,animal被收窄为Dog类型
- animal.bark();
- }
- }
复制代码
自定义类型守卫
可以创建自定义类型守卫函数:
- // 自定义类型守卫
- interface Fish {
- swim(): void;
- }
- interface Bird {
- fly(): void;
- }
- function isFish(pet: Fish | Bird): pet is Fish {
- return (pet as Fish).swim !== undefined;
- }
- function processPet(pet: Fish | Bird) {
- if (isFish(pet)) {
- // 在这个块中,pet被收窄为Fish类型
- pet.swim();
- } else {
- // 在这个块中,pet被收窄为Bird类型
- pet.fly();
- }
- }
复制代码
类型断言的语法和用法
类型断言告诉TypeScript将一个值当作特定类型处理:
- // 类型断言的两种语法
- let someValue: unknown = "hello";
- let strLength: number = (someValue as string).length;
- // 或者
- let strLength2: number = (<string>someValue).length;
- // 类型断言在DOM操作中的常见用法
- const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
- // 或者
- const myCanvas2 = <HTMLCanvasElement>document.getElementById("main_canvas");
- // 类型断言与联合类型
- interface Employee {
- name: string;
- code: number;
- }
- interface Person {
- name: string;
- age: number;
- }
- let employee: Employee | Person = getEmployee(); // 假设这个函数返回Employee或Person
- // 使用类型断言访问特定属性
- if ((employee as Employee).code !== undefined) {
- // 在这个块中,我们知道employee是Employee类型
- console.log((employee as Employee).code);
- }
复制代码
非空断言
非空断言操作符(!)告诉TypeScript一个值不是null或undefined:
- // 非空断言
- function getLength(value: string | null | undefined): number {
- // 使用非空断言
- return value!.length;
- }
- // 但这样更安全
- function getLengthSafe(value: string | null | undefined): number {
- if (value) {
- return value.length;
- }
- return 0;
- }
- // 在DOM操作中的使用
- const button = document.getElementById("submit-button");
- button!.addEventListener("click", () => {
- // 处理点击事件
- });
- // 或者更安全的方式
- const button2 = document.getElementById("submit-button");
- if (button2) {
- button2.addEventListener("click", () => {
- // 处理点击事件
- });
- }
复制代码
明确赋值断言
明确赋值断言(!)告诉TypeScript一个属性会被初始化,即使编译器无法检测到:
- class Point {
- x!: number;
- y!: number;
-
- constructor() {
- this.initialize();
- }
-
- initialize() {
- this.x = 0;
- this.y = 0;
- }
- }
- // 没有明确赋值断言,TypeScript会报错,因为x和y在构造函数中没有初始化
复制代码
实用模式和最佳实践
严格的类型检查配置
在tsconfig.json中启用严格的类型检查选项:
- {
- "compilerOptions": {
- "strict": true,
- "noImplicitAny": true,
- "strictNullChecks": true,
- "strictFunctionTypes": true,
- "strictBindCallApply": true,
- "strictPropertyInitialization": true,
- "noImplicitThis": true,
- "alwaysStrict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noImplicitReturns": true,
- "noFallthroughCasesInSwitch": true
- }
- }
复制代码
不可变数据模式
使用TypeScript的readonly修饰符和Readonly工具类型实现不可变数据:
- // 使用readonly修饰符
- interface ImmutableUser {
- readonly id: number;
- readonly name: string;
- readonly email: string;
- }
- const user: ImmutableUser = {
- id: 1,
- name: "Alice",
- email: "alice@example.com"
- };
- // user.name = "Bob"; // 错误,不能修改只读属性
- // 使用Readonly工具类型
- interface Config {
- apiUrl: string;
- timeout: number;
- retries: number;
- }
- function createConfig(): Readonly<Config> {
- return {
- apiUrl: "https://api.example.com",
- timeout: 5000,
- retries: 3
- };
- }
- const config = createConfig();
- // config.timeout = 10000; // 错误,不能修改只读属性
- // 使用ReadonlyArray
- const numbers: ReadonlyArray<number> = [1, 2, 3];
- // numbers.push(4); // 错误,push方法在ReadonlyArray上不存在
- // numbers[0] = 5; // 错误,不能修改只读数组的元素
复制代码
函数式编程与类型安全
结合函数式编程模式和TypeScript类型系统:
- // 函数组合
- type Func<T, R> = (x: T) => R;
- function compose<T, U, V>(f: Func<U, V>, g: Func<T, U>): Func<T, V> {
- return (x: T) => f(g(x));
- }
- // 使用函数组合
- const toUpperCase = (str: string): string => str.toUpperCase();
- const exclaim = (str: string): string => `${str}!`;
- const shout = compose(exclaim, toUpperCase);
- console.log(shout("hello")); // "HELLO!"
- // 函数式数据结构 - 不可变链表
- type List<T> =
- | { kind: "nil" }
- | { kind: "cons"; head: T; tail: List<T> };
- function length<T>(list: List<T>): number {
- switch (list.kind) {
- case "nil": return 0;
- case "cons": return 1 + length(list.tail);
- }
- }
- function map<T, U>(f: (x: T) => U, list: List<T>): List<U> {
- switch (list.kind) {
- case "nil": return { kind: "nil" };
- case "cons": return {
- kind: "cons",
- head: f(list.head),
- tail: map(f, list.tail)
- };
- }
- }
- // 使用不可变链表
- const list: List<number> = {
- kind: "cons",
- head: 1,
- tail: {
- kind: "cons",
- head: 2,
- tail: { kind: "nil" }
- }
- };
- console.log(length(list)); // 2
- const doubled = map(x => x * 2, list);
- console.log(length(doubled)); // 2
复制代码
错误处理策略
使用类型安全的错误处理模式:
- // Result模式 - 显式处理成功和失败情况
- type Result<T, E> =
- | { kind: "success"; value: T }
- | { kind: "error"; error: E };
- function parseJson<T>(json: string): Result<T, Error> {
- try {
- const parsed = JSON.parse(json);
- return { kind: "success", value: parsed };
- } catch (error) {
- return { kind: "error", error: error as Error };
- }
- }
- // 使用Result模式
- const result = parseJson<{ name: string }>('{"name": "Alice"}');
- if (result.kind === "success") {
- console.log(result.value.name); // 类型安全访问
- } else {
- console.error(result.error.message);
- }
- // Either类型 - 类似于Result但更通用
- type Either<L, R> = Left<L> | Right<R>;
- class Left<L> {
- readonly kind = "left";
- constructor(readonly value: L) {}
- }
- class Right<R> {
- readonly kind = "right";
- constructor(readonly value: R) {}
- }
- function isLeft<L, R>(e: Either<L, R>): e is Left<L> {
- return e.kind === "left";
- }
- function isRight<L, R>(e: Either<L, R>): e is Right<R> {
- return e.kind === "right";
- }
- // 使用Either类型
- function divide(a: number, b: number): Either<string, number> {
- if (b === 0) {
- return new Left("Cannot divide by zero");
- }
- return new Right(a / b);
- }
- const result2 = divide(10, 2);
- if (isRight(result2)) {
- console.log(result2.value); // 5
- }
- const result3 = divide(10, 0);
- if (isLeft(result3)) {
- console.error(result3.value); // "Cannot divide by zero"
- }
复制代码
测试与类型安全
结合TypeScript和测试框架确保代码质量:
- // 使用Jest和TypeScript进行类型安全测试
- import { sum } from './math';
- describe('sum function', () => {
- it('should add two numbers correctly', () => {
- // TypeScript会检查参数类型
- const result = sum(2, 3);
- expect(result).toBe(5);
- });
-
- it('should handle negative numbers', () => {
- const result = sum(-2, -3);
- expect(result).toBe(-5);
- });
-
- // 以下测试会在编译时报错,因为参数类型不匹配
- // it('should not accept strings', () => {
- // const result = sum('2', '3');
- // expect(result).toBe(5);
- // });
- });
- // 使用类型守卫进行运行时类型检查
- function isString(value: unknown): value is string {
- return typeof value === "string";
- }
- function processInput(input: unknown) {
- if (isString(input)) {
- // 在这个块中,TypeScript知道input是string类型
- console.log(input.toUpperCase());
- } else {
- console.error("Input must be a string");
- }
- }
- // 使用zod进行运行时类型验证
- import { z } from "zod";
- const UserSchema = z.object({
- id: z.number(),
- name: z.string(),
- email: z.string().email(),
- });
- type User = z.infer<typeof UserSchema>;
- function parseUser(data: unknown): User {
- return UserSchema.parse(data);
- }
- // 使用示例
- try {
- const user = parseUser({
- id: 1,
- name: "Alice",
- email: "alice@example.com"
- });
- console.log(user.name);
- } catch (error) {
- console.error("Invalid user data", error);
- }
复制代码
工具配置和工程化
tsconfig.json配置详解
一个完整的tsconfig.json配置示例:
- {
- "compilerOptions": {
- /* 基本选项 */
- "target": "ES2020", // 指定ECMAScript目标版本
- "module": "commonjs", // 指定模块代码生成
- "lib": ["ES2020", "DOM", "DOM.Iterable"], // 指定要包含在编译中的库文件
-
- /* 模块解析选项 */
- "moduleResolution": "node", // 决定如何处理模块
- "baseUrl": "./", // 用于解析非绝对模块名称的基目录
- "paths": { // 一系列条目用于重新映射模块导入位置
- "@/*": ["src/*"]
- },
- "esModuleInterop": true, // 通过为所有导入创建命名空间对象,实现CommonJS和ES模块之间的互操作性
-
- /* 严格类型检查选项 */
- "strict": true, // 启用所有严格类型检查选项
- "noImplicitAny": true, // 在表达式和声明上有隐含的any类型时报错
- "strictNullChecks": true, // 严格的null检查
- "strictFunctionTypes": true, // 严格的函数类型检查
- "strictBindCallApply": true, // 严格的bind/call/apply检查
- "strictPropertyInitialization": true, // 严格的类属性初始化检查
- "noImplicitThis": true, // 在this类型为any时报错
- "alwaysStrict": true, // 以严格模式解析并为每个源文件发出"use strict"
-
- /* 额外检查 */
- "noUnusedLocals": true, // 若有未使用的局部变量则报错
- "noUnusedParameters": true, // 若有未使用的参数则报错
- "noImplicitReturns": true, // 不是函数中的所有代码路径都返回值时报错
- "noFallthroughCasesInSwitch": true, // 在switch语句中报告fallthrough错误
-
- /* 高级选项 */
- "skipLibCheck": true, // 跳过对声明文件的类型检查
- "forceConsistentCasingInFileNames": true, // 禁止对同一文件使用不一致的大小写引用
-
- /* 源映射选项 */
- "sourceMap": true, // 生成相应的'.map'文件
- "declaration": true, // 生成相应的'.d.ts'文件
- "declarationMap": true, // 为声明文件生成sourcemap
-
- /* 构建选项 */
- "outDir": "./dist", // 重定向输出目录
- "rootDir": "./src", // 指定输入文件的根目录
-
- /* 实验性选项 */
- "experimentalDecorators": true, // 启用对ES装饰器的实验性支持
- "emitDecoratorMetadata": true // 为装饰器提供元数据支持
- },
- "include": ["src/**/*"], // 包含的文件
- "exclude": ["node_modules", "**/*.spec.ts"] // 排除的文件
- }
复制代码
与构建工具的集成
TypeScript与各种构建工具的集成示例:
- // webpack.config.js 与 TypeScript集成
- const path = require('path');
- module.exports = {
- entry: './src/index.ts',
- module: {
- rules: [
- {
- test: /\.tsx?$/,
- use: 'ts-loader',
- exclude: /node_modules/,
- },
- ],
- },
- resolve: {
- extensions: ['.tsx', '.ts', '.js'],
- },
- output: {
- filename: 'bundle.js',
- path: path.resolve(__dirname, 'dist'),
- },
- };
- // Vite 配置 (vite.config.ts)
- import { defineConfig } from 'vite';
- import react from '@vitejs/plugin-react';
- export default defineConfig({
- plugins: [react()],
- resolve: {
- alias: {
- '@': path.resolve(__dirname, './src'),
- },
- },
- server: {
- port: 3000,
- },
- build: {
- outDir: 'dist',
- sourcemap: true,
- },
- });
- // Rollup 配置 (rollup.config.js)
- import typescript from '@rollup/plugin-typescript';
- import { nodeResolve } from '@rollup/plugin-node-resolve';
- import commonjs from '@rollup/plugin-commonjs';
- export default {
- input: 'src/index.ts',
- output: {
- file: 'dist/bundle.js',
- format: 'cjs',
- },
- plugins: [
- typescript(),
- nodeResolve(),
- commonjs(),
- ],
- };
复制代码
代码检查工具
使用ESLint和Prettier进行代码检查和格式化:
- // .eslintrc.js
- module.exports = {
- parser: '@typescript-eslint/parser',
- plugins: ['@typescript-eslint'],
- extends: [
- 'eslint:recommended',
- '@typescript-eslint/recommended',
- '@typescript-eslint/recommended-requiring-type-checking',
- ],
- parserOptions: {
- ecmaVersion: 2020,
- sourceType: 'module',
- project: './tsconfig.json',
- },
- rules: {
- // 自定义规则
- '@typescript-eslint/no-unused-vars': 'error',
- '@typescript-eslint/explicit-function-return-type': 'warn',
- '@typescript-eslint/no-explicit-any': 'warn',
- },
- };
- // .prettierrc
- {
- "semi": true,
- "trailingComma": "all",
- "singleQuote": true,
- "printWidth": 120,
- "tabWidth": 2
- }
- // package.json中的脚本
- {
- "scripts": {
- "lint": "eslint src --ext .ts,.tsx",
- "lint:fix": "eslint src --ext .ts,.tsx --fix",
- "format": "prettier --write "src/**/*.{ts,tsx}"",
- "type-check": "tsc --noEmit"
- }
- }
复制代码
自动化类型测试
使用TypeScript的API进行自动化类型测试:
- // type-test.ts
- import ts from 'typescript';
- import path from 'path';
- function compileTypeScript(filePath: string) {
- const configPath = ts.findConfigFile(
- process.cwd(),
- ts.sys.fileExists,
- 'tsconfig.json'
- );
-
- if (!configPath) {
- throw new Error('Could not find tsconfig.json');
- }
-
- const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
- const config = ts.parseJsonConfigFileContent(
- configFile.config,
- ts.sys,
- path.dirname(configPath)
- );
-
- const program = ts.createProgram([filePath], config.options);
- const emitResult = program.emit();
-
- const allDiagnostics = ts
- .getPreEmitDiagnostics(program)
- .concat(emitResult.diagnostics);
-
- return allDiagnostics;
- }
- // 运行类型测试
- const diagnostics = compileTypeScript('src/example.ts');
- diagnostics.forEach(diagnostic => {
- if (diagnostic.file) {
- const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
- diagnostic.start!
- );
- const message = ts.flattenDiagnosticMessageText(
- diagnostic.messageText,
- '\n'
- );
- console.log(
- `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
- );
- } else {
- console.log(
- ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')
- );
- }
- });
- // 如果有诊断信息,则退出码为1
- process.exit(diagnostics.length > 0 ? 1 : 0);
复制代码
持续集成中的类型检查
在GitHub Actions中设置TypeScript类型检查:
- # .github/workflows/type-check.yml
- name: Type Check
- on:
- push:
- branches: [ main ]
- pull_request:
- branches: [ main ]
- jobs:
- type-check:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
-
- - name: Setup Node.js
- uses: actions/setup-node@v2
- with:
- node-version: '16'
- cache: 'npm'
-
- - name: Install dependencies
- run: npm ci
-
- - name: Run TypeScript type checking
- run: npm run type-check
-
- - name: Run ESLint
- run: npm run lint
复制代码
总结
TypeScript的类型系统为JavaScript开发者提供了强大的工具,用于编写更安全、更可维护的代码。从基础类型到高级技巧,TypeScript的类型安全特性可以帮助我们在开发过程中捕获潜在的错误,减少运行时bug。
本文涵盖了TypeScript类型系统的各个方面,包括:
1. 基本类型系统和类型注解
2. 接口和类型别名的使用
3. 函数类型安全和泛型
4. 类和泛型的高级应用
5. 高级类型技巧,如联合类型、交叉类型和条件类型
6. 类型守卫和类型断言的使用
7. 实用的类型安全模式和最佳实践
8. 工具配置和工程化实践
通过掌握这些概念和技巧,你可以充分利用TypeScript的类型系统,构建更健壮、更可靠的应用程序。随着TypeScript的不断发展,类型安全编程将成为前端和后端开发的标准实践,帮助我们构建更高质量的软件。
要进一步提升你的TypeScript技能,建议:
1. 深入学习TypeScript官方文档和高级类型系统
2. 参与开源项目,观察大型项目中TypeScript的使用方式
3. 尝试将现有的JavaScript项目迁移到TypeScript
4. 探索TypeScript与其他工具和框架的集成
5. 关注TypeScript社区的最新发展和最佳实践
通过持续学习和实践,你将能够充分利用TypeScript的类型系统,编写出更加安全、可维护的代码,打造出真正无bug的应用程序。 |
|