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

利用TypeScript类型系统构建健壮代码

2022-02-286.7k 阅读

1. 理解TypeScript类型系统基础

1.1 基本类型

TypeScript支持JavaScript所有基本类型,同时为它们提供了更明确的类型定义。例如,number类型表示所有的数字,无论是整数还是浮点数。

let num: number = 42;
num = 3.14;

string类型用于表示文本字符串。

let str: string = "Hello, TypeScript";

boolean类型表示逻辑上的真或假。

let isDone: boolean = true;

1.2 类型推断

TypeScript强大的类型推断机制可以在很多情况下自动推断变量的类型。例如,当变量被赋值时,TypeScript会根据赋值的内容推断其类型。

let inferredNum = 10; // inferredNum被推断为number类型
let inferredStr = "abc"; // inferredStr被推断为string类型

即使在函数返回值中,TypeScript也能进行类型推断。

function add(a, b) {
    return a + b;
}
let result = add(2, 3); // result被推断为number类型

1.3 联合类型

联合类型允许一个变量拥有多种类型。例如,我们可能有一个函数,它可以接受stringnumber类型的参数。

function printValue(value: string | number) {
    console.log(value);
}
printValue(10);
printValue("Hello");

1.4 类型别名

类型别名可以为一个类型定义一个新的名字,这在处理复杂类型时非常有用。比如,我们可以定义一个表示用户性别类型别名。

type Gender = "male" | "female";
let userGender: Gender = "male";

2. 函数中的类型定义

2.1 参数和返回值类型

在TypeScript中,明确函数的参数和返回值类型是构建健壮代码的关键。

function addNumbers(a: number, b: number): number {
    return a + b;
}
let sum = addNumbers(5, 3);

如果参数类型不匹配,TypeScript会抛出错误。例如:

// 报错:Argument of type 'string' is not assignable to parameter of type 'number'
addNumbers("5", 3); 

2.2 可选参数和默认参数

函数可以有可选参数,在参数名后加上?表示该参数是可选的。

function greet(name: string, message?: string) {
    if (message) {
        console.log(`${name}, ${message}`);
    } else {
        console.log(`Hello, ${name}`);
    }
}
greet("John");
greet("Jane", "How are you?");

默认参数也很常见,在参数定义时直接赋值即可。

function greetWithDefault(name: string, message = "How are you?") {
    console.log(`${name}, ${message}`);
}
greetWithDefault("Bob");

2.3 函数重载

函数重载允许一个函数有多个不同的参数列表定义。例如,我们有一个print函数,它可以打印不同类型的数据。

function print(value: string): void;
function print(value: number): void;
function print(value: any) {
    console.log(value);
}
print("Hello");
print(123);

3. 接口(Interfaces)

3.1 定义接口

接口是TypeScript中非常重要的概念,它用于定义对象的形状(shape)。例如,我们定义一个表示用户信息的接口。

interface User {
    name: string;
    age: number;
    email: string;
}
let user: User = {
    name: "Alice",
    age: 25,
    email: "alice@example.com"
};

3.2 可选属性

接口中的属性可以是可选的,在属性名后加上?

interface OptionalUser {
    name: string;
    age?: number;
    email: string;
}
let optionalUser: OptionalUser = {
    name: "Bob",
    email: "bob@example.com"
};

3.3 只读属性

有时候,我们希望对象的某些属性一旦被赋值就不能再更改,这时候可以使用只读属性。

interface ReadonlyUser {
    readonly id: number;
    name: string;
}
let readonlyUser: ReadonlyUser = {
    id: 1,
    name: "Charlie"
};
// 报错:Cannot assign to 'id' because it is a read - only property.
readonlyUser.id = 2; 

3.4 接口继承

接口可以继承其他接口,从而复用已有的属性和方法定义。

interface Admin extends User {
    role: string;
}
let admin: Admin = {
    name: "David",
    age: 30,
    email: "david@example.com",
    role: "admin"
};

4. 类型断言

4.1 语法

类型断言用于告诉TypeScript编译器一个值的类型,即使编译器无法自动推断出来。有两种语法形式:尖括号语法和as语法。 尖括号语法:

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

as语法:

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

4.2 用途

类型断言在处理DOM操作时非常有用。例如,当我们从document.getElementById获取一个元素时,TypeScript只能推断其类型为HTMLElement | null。如果我们确定该元素存在,就可以使用类型断言。

let myDiv = document.getElementById('myDiv');
if (myDiv) {
    let divAsHTMLDivElement = myDiv as HTMLDivElement;
    divAsHTMLDivElement.style.color = 'red';
}

5. 泛型(Generics)

5.1 泛型函数

泛型允许我们在定义函数、接口或类时不指定具体类型,而是在使用时再确定类型。例如,我们定义一个返回数组中第一个元素的泛型函数。

function getFirst<T>(arr: T[]): T | undefined {
    return arr.length > 0 ? arr[0] : undefined;
}
let numbers = [1, 2, 3];
let firstNumber = getFirst(numbers);
let strings = ["a", "b", "c"];
let firstString = getFirst(strings);

5.2 泛型接口

我们也可以定义泛型接口。例如,定义一个表示包含单个值的容器接口。

interface Container<T> {
    value: T;
}
let numberContainer: Container<number> = { value: 42 };
let stringContainer: Container<string> = { value: "Hello" };

5.3 泛型类

泛型同样适用于类。比如,我们创建一个简单的栈类。

class Stack<T> {
    private items: T[] = [];
    push(item: T) {
        this.items.push(item);
    }
    pop(): T | undefined {
        return this.items.pop();
    }
}
let numberStack = new Stack<number>();
numberStack.push(1);
let poppedNumber = numberStack.pop();

5.4 泛型约束

有时候,我们希望泛型类型满足一定的条件,这就需要用到泛型约束。例如,我们希望泛型类型有length属性。

interface Lengthwise {
    length: number;
}
function printLength<T extends Lengthwise>(arg: T) {
    console.log(arg.length);
}
printLength("abc");
printLength([1, 2, 3]);
// 报错:Type 'number' does not satisfy the constraint 'Lengthwise'
printLength(10); 

6. 枚举(Enums)

6.1 数字枚举

枚举是一种用于定义一组命名常量的方式。数字枚举是最常见的类型。

enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}
let myDirection = Direction.Up;

在这个例子中,Down会自动赋值为2,Left为3,Right为4。

6.2 字符串枚举

字符串枚举允许我们使用字符串作为枚举值。

enum Status {
    Success = "success",
    Failure = "failure"
}
let operationStatus = Status.Success;

6.3 异构枚举(不推荐)

异构枚举是指枚举值既有数字又有字符串类型,但这种方式会使代码的可读性和维护性变差,不推荐使用。

enum MixedEnum {
    First = 1,
    Second = "two"
}

7. 高级类型

7.1 交叉类型(Intersection Types)

交叉类型将多个类型合并为一个类型,新类型包含所有类型的属性和方法。例如,我们有一个User接口和一个Admin接口,通过交叉类型可以创建一个AdminUser类型。

interface User {
    name: string;
    age: number;
}
interface Admin {
    role: string;
}
type AdminUser = User & Admin;
let adminUser: AdminUser = {
    name: "Eve",
    age: 28,
    role: "admin"
};

7.2 映射类型

映射类型允许我们基于现有的类型创建新类型。例如,我们有一个User接口,想要创建一个所有属性都变为只读的新类型。

interface User {
    name: string;
    age: number;
}
type ReadonlyUser = {
    readonly [P in keyof User]: User[P];
};
let readonlyUser: ReadonlyUser = {
    name: "Frank",
    age: 32
};
// 报错:Cannot assign to 'name' because it is a read - only property.
readonlyUser.name = "New Name"; 

7.3 条件类型

条件类型允许我们根据类型关系选择不同的类型。例如,我们定义一个IfString类型,根据传入的类型是否为string来返回不同的类型。

type IfString<T, A, B> = T extends string ? A : B;
type StringResult = IfString<string, "is string", "is not string">;
type NumberResult = IfString<number, "is string", "is not string">;

7.4 索引类型

索引类型允许我们通过索引访问对象的属性类型。例如,我们有一个User接口,想要获取name属性的类型。

interface User {
    name: string;
    age: number;
}
type NameType = User["name"]; // NameType为string类型

8. 使用TypeScript构建健壮前端代码的实践

8.1 在React中的应用

在React项目中使用TypeScript,可以大大提高代码的健壮性。首先,我们可以为组件的props定义接口。

import React from 'react';
interface ButtonProps {
    label: string;
    onClick: () => void;
    disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({ label, onClick, disabled = false }) => {
    return (
        <button disabled={disabled} onClick={onClick}>
            {label}
        </button>
    );
};
export default Button;

这样,在使用Button组件时,如果props不匹配,TypeScript会给出错误提示。

8.2 在Vue中的应用

在Vue项目中,也可以充分利用TypeScript。例如,我们可以为Vue组件的数据和方法定义类型。

import Vue from 'vue';
interface UserData {
    name: string;
    age: number;
}
export default Vue.extend({
    data(): UserData {
        return {
            name: 'Guest',
            age: 0
        };
    },
    methods: {
        incrementAge() {
            this.age++;
        }
    }
});

8.3 处理异步操作

在处理异步操作如Promise时,TypeScript也能提供很好的类型支持。

function fetchData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data fetched');
        }, 1000);
    });
}
fetchData().then((data) => {
    console.log(data);
});

如果fetchData返回的Promise类型与then回调中期望的类型不匹配,TypeScript会报错。

通过深入理解和应用TypeScript的类型系统,我们能够在前端开发中构建更加健壮、可维护的代码。无论是小型项目还是大型企业级应用,TypeScript的类型系统都能帮助我们减少运行时错误,提高开发效率。