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

TypeScript助力Angular开发基础讲解

2021-05-111.9k 阅读

一、TypeScript 基础概述

1.1 什么是 TypeScript

TypeScript 是由微软开发的一款开源的编程语言,它是 JavaScript 的超集,这意味着任何合法的 JavaScript 代码都是合法的 TypeScript 代码。TypeScript 为 JavaScript 添加了静态类型系统,让开发者可以在代码编写阶段就发现类型相关的错误,而不是在运行时才暴露问题。

例如,在 JavaScript 中定义变量:

let num;
num = 'hello';

这里 num 变量一开始没有明确类型,后面赋值为字符串类型,在运行时才可能发现与预期不符的问题。

而在 TypeScript 中:

let num: number;
num = 'hello'; // 这里会报错,因为类型不匹配

1.2 类型注解

TypeScript 中的类型注解是一种轻量级的为函数或变量添加约束的方式。

  • 基本类型注解
    • 数字类型number 表示所有数字,包括整数和浮点数。
    let age: number = 25;
    
    • 字符串类型string 表示文本数据。
    let name: string = 'John';
    
    • 布尔类型boolean 只有 truefalse 两个值。
    let isDone: boolean = false;
    
  • 数组类型注解
    • 类型 + 方括号:表示特定类型的数组。
    let numbers: number[] = [1, 2, 3];
    
    • 泛型数组类型Array<类型>
    let names: Array<string> = ['Alice', 'Bob'];
    
  • 函数类型注解
    • 参数类型和返回值类型
    function add(a: number, b: number): number {
        return a + b;
    }
    
    • 可选参数:在参数名后加 ?
    function greet(name: string, message?: string): void {
        if (message) {
            console.log(`${name}, ${message}`);
        } else {
            console.log(`Hello, ${name}`);
        }
    }
    

1.3 接口(Interfaces)

接口在 TypeScript 中用于定义对象的形状(Shape),它可以描述对象的属性和方法。

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

let user: User = {
    name: 'Jane',
    age: 30,
    email: 'jane@example.com'
};

接口也可以定义函数类型:

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

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

1.4 类(Classes)

TypeScript 支持面向对象编程中的类。类可以包含属性、方法、构造函数等。

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}

class Dog extends Animal {
    breed: string;
    constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }
    speak() {
        console.log(`${this.name} barks.`);
    }
}

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

二、TypeScript 与 Angular 的集成

2.1 Angular 项目初始化与 TypeScript

当使用 Angular CLI 初始化一个新的 Angular 项目时,TypeScript 已经默认集成其中。

  1. 安装 Angular CLI:如果尚未安装,通过 npm 安装。
npm install -g @angular/cli
  1. 初始化项目
ng new my - app

在生成的项目结构中,src 目录下的 .ts 文件就是 TypeScript 代码文件。例如,app.component.ts 定义了应用的根组件:

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

@Component({
    selector: 'app - root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    title = 'my - app';
}

2.2 组件中的 TypeScript 类型定义

在 Angular 组件中,TypeScript 用于定义组件的属性、方法和生命周期钩子函数。

  • 属性类型定义
import { Component } from '@angular/core';

@Component({
    selector: 'app - user - profile',
    templateUrl: './user - profile.component.html',
    styleUrls: ['./user - profile.component.css']
})
export class UserProfileComponent {
    user: { name: string; age: number };
    constructor() {
        this.user = { name: 'Tom', age: 28 };
    }
}
  • 方法类型定义
import { Component } from '@angular/core';

@Component({
    selector: 'app - calculator',
    templateUrl: './calculator.component.html',
    styleUrls: ['./calculator.component.css']
})
export class CalculatorComponent {
    add(a: number, b: number): number {
        return a + b;
    }
}

2.3 服务中的 TypeScript

Angular 服务是一种可注入的类,用于在应用中共享数据和功能。TypeScript 同样用于定义服务的接口和实现。

  1. 创建服务:使用 Angular CLI。
ng generate service user
  1. 服务代码示例
import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class UserService {
    private users: { name: string; age: number }[] = [];

    addUser(user: { name: string; age: number }) {
        this.users.push(user);
    }

    getUsers(): { name: string; age: number }[] {
        return this.users;
    }
}

三、TypeScript 在 Angular 模板中的应用

3.1 模板表达式中的类型推断

在 Angular 模板中,TypeScript 的类型信息有助于模板表达式的正确使用。 例如,在组件中有一个数组属性:

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

@Component({
    selector: 'app - list',
    templateUrl: './list.component.html',
    styleUrls: ['./list.component.css']
})
export class ListComponent {
    numbers: number[] = [1, 2, 3];
}

在模板 list.component.html 中:

<ul>
    <li *ngFor="let num of numbers">{{num}}</li>
</ul>

由于 numbersnumber 类型的数组,模板在遍历和显示时能正确处理 num 的类型。

3.2 模板引用变量与类型

模板引用变量可以在模板中引用 DOM 元素或组件实例。TypeScript 可以帮助确定这些引用的类型。 在组件模板 app.component.html 中:

<input #inputElement type="text">
<button (click)="printValue(inputElement)">Print</button>

在组件 app.component.ts 中:

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

@Component({
    selector: 'app - root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    printValue(input: HTMLInputElement) {
        console.log(input.value);
    }
}

这里通过 #inputElement 创建了一个模板引用变量,printValue 方法参数类型为 HTMLInputElement,明确了输入框元素的类型。

3.3 指令中的类型处理

Angular 指令(如内置指令 ngIfngFor 等)与 TypeScript 类型紧密相关。 例如,ngFor 指令要求迭代的对象是可迭代类型。

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

@Component({
    selector: 'app - iterate',
    templateUrl: './iterate.component.html',
    styleUrls: ['./iterate.component.css']
})
export class IterateComponent {
    items: string[] = ['a', 'b', 'c'];
}

iterate.component.html 中:

<ul>
    <li *ngFor="let item of items">{{item}}</li>
</ul>

因为 itemsstring 类型的数组,符合 ngFor 对可迭代对象的类型要求。

四、TypeScript 高级特性在 Angular 中的应用

4.1 泛型(Generics)

泛型允许我们创建可复用的组件、服务和函数,同时保持类型安全。 在 Angular 服务中使用泛型示例:

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

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

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

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

在组件中使用:

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

@Component({
    selector: 'app - generic - usage',
    templateUrl: './generic - usage.component.html',
    styleUrls: ['./generic - usage.component.css']
})
export class GenericUsageComponent {
    constructor(private genericService: GenericService<number>) {
        this.genericService.add(1);
        this.genericService.add(2);
        console.log(this.genericService.get());
    }
}

4.2 类型守卫(Type Guards)

类型守卫是一种运行时检查,用于确保某个变量在特定代码块内具有特定类型。 在 Angular 组件中示例:

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

@Component({
    selector: 'app - type - guard',
    templateUrl: './type - guard.component.html',
    styleUrls: ['./type - guard.component.css']
})
export class TypeGuardComponent {
    data: string | number;

    constructor() {
        this.data = 10;
    }

    printData() {
        if (typeof this.data ==='string') {
            console.log(this.data.length);
        } else {
            console.log(this.data.toFixed(2));
        }
    }
}

这里 typeof this.data ==='string' 就是一个类型守卫,确保在不同分支中 this.data 的类型安全。

4.3 装饰器(Decorators)

Angular 大量使用装饰器来定义组件、服务、指令等。TypeScript 装饰器是一种特殊的声明,它可以附加到类声明、方法、访问器、属性或参数上。

  • 组件装饰器
import { Component } from '@angular/core';

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

@Component 装饰器用于定义组件的元数据,如选择器、模板和样式等。

  • 服务装饰器
import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class MyService {
    // 服务逻辑
}

@Injectable 装饰器用于标记服务类,使其可被注入到其他组件或服务中。

五、TypeScript 与 Angular 的最佳实践

5.1 保持类型定义清晰

在 Angular 项目中,对组件的输入属性、服务的返回值等都要明确类型定义。 例如,在组件中定义输入属性:

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

@Component({
    selector: 'app - product - card',
    templateUrl: './product - card.component.html',
    styleUrls: ['./product - card.component.css']
})
export class ProductCardComponent {
    @Input() product: { name: string; price: number };
}

这样可以避免在模板中使用 product 时出现类型错误。

5.2 使用接口进行数据交互

在服务与组件之间传递数据时,使用接口来定义数据结构。 例如,在服务中:

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;
    }
}

在组件中:

import { Component } from '@angular/core';
import { UserService, User } from './user.service';

@Component({
    selector: 'app - user - list',
    templateUrl: './user - list.component.html',
    styleUrls: ['./user - list.component.css']
})
export class UserListComponent {
    users: User[];
    constructor(private userService: UserService) {
        this.users = this.userService.getUsers();
    }
}

5.3 遵循命名规范

在 TypeScript 代码中,遵循 Angular 的命名规范有助于提高代码的可读性和可维护性。

  • 组件命名:采用 app - 组件名 - component 的形式,如 app - header - component
  • 服务命名:采用 服务名 - service 的形式,如 user - service

5.4 利用 TypeScript 进行代码重构

随着项目的发展,TypeScript 的类型系统可以帮助我们更安全地进行代码重构。例如,当修改一个服务的返回值类型时,TypeScript 会在使用该服务的组件中提示类型错误,从而让我们能及时更新相关代码。 假设原来的 UserService 返回的 User 接口没有 phone 属性:

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

现在需要添加 phone 属性:

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

在使用 UserService 的组件中,如果之前没有正确处理 phone 属性,TypeScript 会报错,提醒开发者进行相应修改。

六、常见问题与解决方法

6.1 类型不匹配错误

在 Angular 项目中,经常会遇到类型不匹配的错误,比如在组件模板中使用了与组件属性类型不一致的值。 例如,组件属性定义为 number 类型:

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

@Component({
    selector: 'app - numeric - display',
    templateUrl: './numeric - display.component.html',
    styleUrls: ['./numeric - display.component.css']
})
export class NumericDisplayComponent {
    value: number;
    constructor() {
        this.value = 10;
    }
}

但在模板 numeric - display.component.html 中:

<p>{{value + 'abc'}}</p> <!-- 这里会报错,因为字符串不能与数字相加 -->

解决方法是确保模板表达式中的操作与属性类型匹配,比如:

<p>{{value + 5}}</p>

6.2 模块导入问题

有时在导入 Angular 模块或自定义模块时,会遇到找不到模块的错误。 例如,自定义了一个服务 custom - service.ts

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

@Injectable({
    providedIn: 'root'
})
export class CustomService {
    // 服务逻辑
}

在组件中导入时:

import { Component } from '@angular/core';
import { CustomService } from './custom - service'; // 如果路径错误会报错

@Component({
    selector: 'app - custom - usage',
    templateUrl: './custom - usage.component.html',
    styleUrls: ['./custom - usage.component.css']
})
export class CustomUsageComponent {
    constructor(private customService: CustomService) {
        // 使用服务
    }
}

解决方法是仔细检查导入路径是否正确,特别是在项目结构复杂时,注意相对路径和绝对路径的使用。

6.3 装饰器相关问题

在使用 Angular 装饰器时,可能会遇到装饰器参数配置错误等问题。 例如,在定义组件时,@Component 装饰器的 selector 属性配置错误:

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

@Component({
    selector: 'app:my - wrong - selector', // 这里冒号使用错误
    templateUrl: './my - component.component.html',
    styleUrls: ['./my - component.component.css']
})
export class MyComponent {
    // 组件逻辑
}

解决方法是查阅 Angular 文档,确保装饰器参数的正确使用,selector 通常使用连字符 - 分隔单词,如 app - my - selector

七、性能优化与 TypeScript

7.1 类型检查对性能的影响

TypeScript 的类型检查主要在编译阶段进行,这有助于发现潜在的错误,但在大型项目中,过多复杂的类型检查可能会略微增加编译时间。 为了优化编译性能,可以合理使用 // @ts - ignore 注释来忽略一些暂时无法解决但不影响运行的类型错误,但应谨慎使用,避免隐藏真正的问题。 例如:

// @ts - ignore
let someValue: number = 'not a number'; // 暂时忽略类型错误

7.2 优化数据类型选择

在 Angular 应用中,选择合适的数据类型可以提高性能。例如,对于固定长度且类型相同的数据,使用 TypedArray(如 Uint8ArrayFloat32Array 等)可以比普通数组更高效地存储和操作数据,特别是在处理大量数据时。

let numbers = new Uint8Array([1, 2, 3]);

7.3 代码结构优化与类型管理

良好的代码结构有助于 TypeScript 类型管理和性能优化。将相关的功能封装在独立的模块和服务中,通过明确的接口和类型定义进行交互,可以减少不必要的类型转换和错误,提高代码的可维护性和运行效率。 例如,将用户相关的操作封装在 UserService 中,通过清晰的接口定义 User 类型来进行数据交互:

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

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

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

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