MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

掌握TypeScript为Angular开发提速

2023-01-037.4k 阅读

理解 TypeScript 基础

静态类型系统

TypeScript 最核心的特性之一就是它的静态类型系统。与 JavaScript 的动态类型不同,在 TypeScript 中,变量、函数参数和返回值等都可以指定类型。这在开发过程中提供了早期错误检查机制,避免在运行时才发现类型不匹配的问题。

例如,在 JavaScript 中可以这样写:

function add(a, b) {
    return a + b;
}
add(1, '2');

这段代码在 JavaScript 中不会报错,运行时会将数字 1 强制转换为字符串并进行拼接,结果为 '12',这可能不是开发者预期的行为。

而在 TypeScript 中,我们可以指定参数类型:

function add(a: number, b: number): number {
    return a + b;
}
add(1, '2'); // 这里会报错,因为第二个参数的类型应为 number 而不是 string

这样在编译阶段就会捕获到类型错误,有助于在开发过程中更早地发现和修复问题。

类型声明与推断

  1. 类型声明:TypeScript 允许显式地声明变量的类型。例如:
let num: number = 10;
let str: string = 'Hello';
let isDone: boolean = false;

通过类型声明,明确告知编译器变量所预期的数据类型。

  1. 类型推断:TypeScript 强大的类型推断功能可以在很多情况下自动推断出变量的类型,无需显式声明。比如:
let num = 10; // TypeScript 推断 num 为 number 类型
let arr = [1, 2, 3]; // 推断 arr 为 number 数组类型

当变量的初始值明确时,TypeScript 能够准确推断出其类型,减少了不必要的类型声明,使代码更加简洁。

接口(Interfaces)

接口在 TypeScript 中用于定义对象的形状(shape),即对象应该具有哪些属性和方法,以及这些属性和方法的类型。

定义一个简单的接口:

interface User {
    name: string;
    age: number;
}

function greet(user: User) {
    return `Hello, ${user.name}! You are ${user.age} years old.`;
}

let tom: User = {
    name: 'Tom',
    age: 25
};

console.log(greet(tom));

在上述代码中,User 接口定义了 name 为字符串类型,age 为数字类型。greet 函数接受一个符合 User 接口形状的对象作为参数。这样可以确保传递给 greet 函数的对象具有正确的属性和类型,增强了代码的健壮性。

接口还可以用于定义函数类型:

interface AddFunction {
    (a: number, b: number): number;
}

let add: AddFunction = function(a, b) {
    return a + b;
};

这里 AddFunction 接口定义了一个函数类型,该函数接受两个 number 类型的参数并返回一个 number 类型的值。

类(Classes)

TypeScript 对面向对象编程提供了完整的支持,类是其重要组成部分。

  1. 类的基本定义
class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}

let dog = new Animal('Buddy');
dog.speak();

在这个 Animal 类中,定义了一个 name 属性和一个构造函数 constructor 用于初始化 name 属性,还定义了一个 speak 方法。

  1. 继承:类可以通过 extends 关键字实现继承。
class Dog extends Animal {
    breed: string;
    constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }
    speak() {
        console.log(`${this.name}, a ${this.breed}, barks.`);
    }
}

let myDog = new Dog('Max', 'Golden Retriever');
myDog.speak();

Dog 类继承自 Animal 类,拥有 Animal 类的属性和方法,并添加了自己的 breed 属性和重写了 speak 方法。

  1. 访问修饰符:TypeScript 提供了 publicprivateprotected 三种访问修饰符。
    • public:默认修饰符,类的成员在类内外都可以访问。
    • private:私有成员,只能在类内部访问。
    • protected:受保护成员,只能在类内部及其子类中访问。
class BankAccount {
    private balance: number;
    constructor(initialBalance: number) {
        this.balance = initialBalance;
    }
    deposit(amount: number) {
        this.balance += amount;
    }
    getBalance(): number {
        return this.balance;
    }
}

let account = new BankAccount(1000);
// console.log(account.balance); // 报错,balance 是私有属性,无法在类外部访问
account.deposit(500);
console.log(account.getBalance());

BankAccount 类中,balance 属性被声明为 private,只能通过类内部的方法 depositgetBalance 来访问和修改。

在 Angular 中应用 TypeScript

Angular 项目初始化与 TypeScript 配置

  1. 初始化 Angular 项目:使用 Angular CLI 初始化一个新的 Angular 项目时,TypeScript 是默认的开发语言。
ng new my - app

这个命令会创建一个新的 Angular 项目结构,并自动配置好 TypeScript 相关的文件和设置。

  1. TypeScript 配置文件(tsconfig.json):在项目根目录下的 tsconfig.json 文件中,可以对 TypeScript 的编译选项进行配置。
{
    "compileOnSave": false,
    "compilerOptions": {
        "baseUrl": "./",
        "outDir": "./dist/out - tsc",
        "sourceMap": true,
        "declaration": false,
        "downlevelIteration": true,
        "experimentalDecorators": true,
        "module": "esnext",
        "moduleResolution": "node",
        "importHelpers": true,
        "target": "es2015",
        "lib": [
            "es2018",
            "dom"
        ],
        "skipLibCheck": true,
        "strict": true
    }
}

其中一些重要的选项: - strict:开启严格模式,启用所有严格类型检查选项,有助于发现更多潜在的类型错误。 - module:指定生成的模块系统,esnext 表示使用 ECMAScript 模块。 - target:指定编译后的 JavaScript 版本,es2015 即 ES6。

组件开发中的 TypeScript 应用

  1. 组件类定义:在 Angular 中,组件是核心构建块。使用 TypeScript 定义组件类可以充分利用其类型系统。
import { Component } from '@angular/core';

@Component({
    selector: 'app - my - component',
    templateUrl: './my - component.html',
    styleUrls: ['./my - component.css']
})
export class MyComponent {
    title: string = 'My Angular Component';
    count: number = 0;

    increment() {
        this.count++;
    }
}

在这个组件类 MyComponent 中,定义了 titlecount 两个属性,分别为字符串和数字类型,还定义了一个 increment 方法用于增加 count 的值。通过明确的类型定义,使得代码的意图更加清晰,同时在开发过程中可以得到编译器的类型检查支持。

  1. 组件交互与类型安全:当组件之间进行数据传递时,TypeScript 的类型系统确保数据的正确传递。

例如,父组件向子组件传递数据: 父组件模板(parent.component.html):

<app - child - component [data]="parentData"></app - child - component>

父组件类(parent.component.ts):

import { Component } from '@angular/core';

@Component({
    selector: 'app - parent - component',
    templateUrl: './parent.component.html'
})
export class ParentComponent {
    parentData: string = 'Hello from parent';
}

子组件类(child.component.ts):

import { Component, Input } from '@angular/core';

@Component({
    selector: 'app - child - component',
    templateUrl: './child.component.html'
})
export class ChildComponent {
    @Input() data: string;
}

在子组件 ChildComponent 中,使用 @Input() 装饰器定义了一个 data 属性,类型为 string。这样确保了父组件传递过来的数据类型是正确的,如果传递了非字符串类型的数据,TypeScript 编译器会报错。

服务开发中的 TypeScript 应用

  1. 服务类定义:Angular 服务用于在应用中共享数据和功能。使用 TypeScript 定义服务类同样可以受益于其类型系统。
import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class DataService {
    private data: string[] = [];

    addItem(item: string) {
        this.data.push(item);
    }

    getItems(): string[] {
        return this.data;
    }
}

DataService 服务类中,定义了一个私有属性 data 为字符串数组,以及 addItemgetItems 两个方法,通过明确的类型定义,使得服务的功能和数据结构更加清晰,同时保证了类型安全。

  1. 服务注入与类型匹配:在组件中注入服务时,TypeScript 确保注入的服务类型正确。
import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
    selector: 'app - my - component',
    templateUrl: './my - component.html'
})
export class MyComponent {
    constructor(private dataService: DataService) {}

    ngOnInit() {
        this.dataService.addItem('New item');
        console.log(this.dataService.getItems());
    }
}

MyComponent 组件的构造函数中注入了 DataService,TypeScript 确保注入的 dataServiceDataService 类型的实例,避免了注入错误类型服务的问题。

RxJS 与 TypeScript 的结合

  1. Observable 类型定义:RxJS 是 Angular 中用于处理异步操作和事件流的库。在 TypeScript 中,Observable 有明确的类型定义。
import { Observable } from 'rxjs';

function getData(): Observable<string> {
    return new Observable(observer => {
        setTimeout(() => {
            observer.next('Data from observable');
            observer.complete();
        }, 2000);
    });
}

getData().subscribe(data => {
    console.log(data);
});

getData 函数中,返回一个 Observable<string>,表示这个可观察对象会发出字符串类型的值。这样在订阅该 Observable 时,TypeScript 可以确保处理的数据类型是正确的。

  1. 操作符与类型推断:RxJS 的操作符在 TypeScript 中也能很好地进行类型推断。
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

function getNumbers(): Observable<number[]> {
    return new Observable(observer => {
        observer.next([1, 2, 3]);
        observer.complete();
    });
}

getNumbers()
   .pipe(map(numbers => numbers.map(num => num * 2)))
   .subscribe(result => {
        console.log(result);
    });

在上述代码中,map 操作符对 Observable<number[]> 进行操作,TypeScript 能够正确推断出经过 map 操作后返回的 Observable 类型仍然是 number 数组,只是数组中的每个元素都经过了 num * 2 的计算。

利用 TypeScript 优化 Angular 开发流程

代码可读性与可维护性提升

  1. 类型注释增强代码可读性:在 Angular 代码中,通过 TypeScript 的类型注释,变量、函数参数和返回值的类型一目了然。
import { Component } from '@angular/core';

@Component({
    selector: 'app - complex - component',
    templateUrl: './complex - component.html'
})
export class ComplexComponent {
    userData: { name: string; age: number };

    constructor() {
        this.userData = { name: 'John', age: 30 };
    }

    updateUser(updatedData: { name: string; age: number }) {
        this.userData = updatedData;
    }
}

ComplexComponent 中,userData 属性和 updateUser 函数的参数都有明确的类型定义,即使代码逻辑较为复杂,开发人员也能快速理解数据的结构和函数的预期输入,提高了代码的可读性。

  1. 接口和类型别名简化代码维护:使用接口和类型别名可以将复杂的数据结构抽象出来,便于在多个组件或服务中复用,同时在需要修改数据结构时,只需要在一处进行修改。
interface User {
    name: string;
    age: number;
}

interface Role {
    roleName: string;
    permissions: string[];
}

class UserService {
    private users: User[] = [];
    private roles: Role[] = [];

    addUser(user: User) {
        this.users.push(user);
    }

    addRole(role: Role) {
        this.roles.push(role);
    }
}

如果后续需要修改 UserRole 的结构,只需要修改对应的接口定义,所有使用这些接口的地方都会自动更新,大大简化了代码的维护工作。

减少运行时错误

  1. 类型检查提前发现错误:TypeScript 的静态类型系统在编译阶段进行类型检查,能够提前发现许多在 JavaScript 中运行时才会暴露的错误。
import { Component } from '@angular/core';

@Component({
    selector: 'app - error - prone - component',
    templateUrl: './error - prone - component.html'
})
export class ErrorProneComponent {
    num: number;

    constructor() {
        this.num = 'ten'; // 这里会在编译时报错,因为类型不匹配
    }
}

在上述代码中,将字符串 'ten' 赋值给 number 类型的 num 变量,TypeScript 编译器会立即报错,避免了在运行时才发现这个错误,提高了代码的稳定性。

  1. 严格模式避免潜在错误:启用 tsconfig.json 中的 strict 模式,TypeScript 会进行更严格的类型检查,包括对空值的检查等,进一步减少潜在的运行时错误。
function printLength(str: string) {
    console.log(str.length);
}

let maybeString: string | null = null;
// printLength(maybeString); // 在 strict 模式下会报错,因为 maybeString 可能为 null
if (maybeString) {
    printLength(maybeString);
}

在严格模式下,不能直接将可能为 nullundefined 的值传递给期望非空值的函数,需要进行额外的检查,从而避免了运行时因空值导致的错误。

提高团队协作效率

  1. 清晰的代码结构便于理解:TypeScript 的类型系统使得代码结构更加清晰,团队成员能够快速理解彼此的代码意图。新加入的成员可以通过类型注释和接口定义快速了解项目的数据结构和函数功能,减少学习成本。
// user - service.ts
import { Injectable } from '@angular/core';

interface User {
    name: string;
    email: string;
}

@Injectable({
    providedIn: 'root'
})
export class UserService {
    private users: User[] = [];

    addUser(user: User) {
        this.users.push(user);
    }

    getUsers(): User[] {
        return this.users;
    }
}

对于团队中的其他开发人员来说,通过阅读 UserService 的代码,能够清楚地知道 User 的结构以及 addUsergetUsers 函数的功能和输入输出类型,便于进行后续的开发和维护工作。

  1. 一致的编码规范:TypeScript 鼓励遵循一定的编码规范,如使用接口、类和命名约定等。团队可以制定统一的 TypeScript 编码规范,使得整个项目的代码风格一致,便于代码的审查和维护。例如,统一使用 PascalCase 命名类和接口,使用 camelCase 命名变量和函数等。

深入 TypeScript 高级特性在 Angular 中的应用

泛型(Generics)

  1. 泛型基础概念:泛型是 TypeScript 中一个强大的特性,它允许我们在定义函数、接口或类时使用类型参数,使得这些组件可以适用于多种类型,而不是具体的某一种类型。
function identity<T>(arg: T): T {
    return arg;
}

let result1 = identity<number>(10);
let result2 = identity<string>('Hello');

identity 函数中,<T> 是类型参数,它可以代表任何类型。调用函数时,可以通过 <number><string> 等指定具体的类型。

  1. 在 Angular 服务中应用泛型:在 Angular 服务中,泛型可以用于创建通用的数据访问层。
import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class GenericService<T> {
    private data: T[] = [];

    addItem(item: T) {
        this.data.push(item);
    }

    getItems(): T[] {
        return this.data;
    }
}

这个 GenericService 服务类可以用于存储和管理任何类型的数据。例如,可以创建一个用于存储数字的实例,也可以创建一个用于存储字符串的实例。

import { Component } from '@angular/core';
import { GenericService } from './generic.service';

@Component({
    selector: 'app - generic - component',
    templateUrl: './generic - component.html'
})
export class GenericComponent {
    constructor(private numberService: GenericService<number>, private stringService: GenericService<string>) {
        this.numberService.addItem(1);
        this.stringService.addItem('one');
    }
}

通过泛型,提高了服务的复用性,减少了重复代码。

装饰器(Decorators)

  1. 装饰器基础:装饰器是一种特殊类型的声明,它可以附加到类声明、方法、访问器、属性或参数上。在 TypeScript 中,装饰器使用 @ 符号表示。
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        console.log(`Calling method ${propertyKey} with arguments:`, args);
        const result = originalMethod.apply(this, args);
        console.log(`Method ${propertyKey} returned:`, result);
        return result;
    };
    return descriptor;
}

class MathUtils {
    @log
    add(a: number, b: number) {
        return a + b;
    }
}

let math = new MathUtils();
math.add(2, 3);

在上述代码中,log 是一个装饰器函数,它被应用到 MathUtils 类的 add 方法上。装饰器可以在不修改原方法逻辑的情况下,添加额外的功能,如日志记录。

  1. Angular 中的装饰器应用:Angular 大量使用了装饰器来定义组件、服务、模块等。
import { Component } from '@angular/core';

@Component({
    selector: 'app - my - component',
    templateUrl: './my - component.html',
    styleUrls: ['./my - component.css']
})
export class MyComponent {
    // 组件逻辑
}

@Component 装饰器用于定义一个 Angular 组件,它接收一个配置对象,包含组件的选择器、模板文件路径、样式文件路径等信息。同样,@Injectable 装饰器用于定义 Angular 服务,@NgModule 装饰器用于定义 Angular 模块等。通过装饰器,Angular 代码的结构和功能定义更加清晰和简洁。

类型守卫(Type Guards)

  1. 类型守卫概念:类型守卫是一个返回 boolean 类型的函数,其返回值能够告诉 TypeScript 在某个特定的代码块中,某个变量的类型是什么。
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));
    }
}

printValue 函数中,isString 函数作为类型守卫,通过 value is string 的语法明确表示如果 isString 返回 true,那么在对应的 if 代码块中,value 的类型就是 string,这样就可以安全地访问 string 类型的属性和方法。

  1. 在 Angular 组件中应用类型守卫:在 Angular 组件中,类型守卫可以用于处理从服务或其他组件接收到的可能为多种类型的数据。
import { Component } from '@angular/core';

interface User {
    name: string;
    age: number;
}

interface Admin {
    name: string;
    role: string;
}

function isAdmin(user: User | Admin): user is Admin {
    return (user as Admin).role!== undefined;
}

@Component({
    selector: 'app - user - component',
    templateUrl: './user - component.html'
})
export class UserComponent {
    user: User | Admin;

    constructor() {
        this.user = { name: 'John', role: 'admin' };
    }

    displayInfo() {
        if (isAdmin(this.user)) {
            console.log(`Admin ${this.user.name} has role ${this.user.role}`);
        } else {
            console.log(`User ${this.user.name} is ${this.user.age} years old`);
        }
    }
}

UserComponent 中,isAdmin 类型守卫用于判断 user 对象是 User 类型还是 Admin 类型,从而在 displayInfo 方法中进行不同的处理,确保了类型安全。