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

从JavaScript到TypeScript:转型过程中的关键点

2021-04-096.5k 阅读

理解 TypeScript 的基本概念

TypeScript 是 JavaScript 的超集,它在 JavaScript 的基础上增加了静态类型系统。这意味着在 TypeScript 中,我们可以为变量、函数参数和返回值指定类型。例如:

let age: number = 30;

在上述代码中,我们声明了一个变量 age,并指定它的类型为 number。如果我们尝试给 age 赋值一个非数字类型的值,TypeScript 编译器就会报错。

类型注解与类型推断

类型注解是我们显式指定变量或函数参数类型的方式。而类型推断则是 TypeScript 编译器根据代码上下文自动推断出类型的机制。例如:

// 类型推断
let num = 10; // num 的类型被推断为 number

// 类型注解
let str: string = 'Hello';

在大多数情况下,TypeScript 的类型推断足够智能,能准确推断出变量的类型。但在一些复杂场景下,我们可能需要使用类型注解来明确类型。

基本类型

TypeScript 支持与 JavaScript 相同的基本类型,如 numberstringbooleannullundefined,此外还增加了 voidanynever 等类型。

  • void:表示没有任何类型,通常用于函数没有返回值的情况。
function printMessage(): void {
    console.log('Hello');
}
  • any:表示任意类型,当我们不确定一个值的类型时,可以使用 any。但过度使用 any 会失去 TypeScript 类型检查的优势。
let value: any = 'Hello';
value = 10;
  • never:表示永远不会出现的值的类型。通常用于函数永远不会返回或总是抛出异常的情况。
function throwError(message: string): never {
    throw new Error(message);
}

函数中的类型声明

在 TypeScript 中,我们可以为函数的参数和返回值指定类型。

function addNumbers(a: number, b: number): number {
    return a + b;
}

上述函数 addNumbers 接受两个 number 类型的参数,并返回一个 number 类型的值。如果我们调用这个函数时传入非 number 类型的参数,TypeScript 编译器会报错。

可选参数与默认参数

在 TypeScript 中,我们可以定义函数的可选参数和默认参数。

function greet(name: string, message?: string): void {
    if (message) {
        console.log(`${name}, ${message}`);
    } else {
        console.log(`Hello, ${name}`);
    }
}

// 调用函数
greet('John');
greet('Jane', 'How are you?');

// 默认参数
function multiply(a: number, b: number = 2): number {
    return a * b;
}

// 调用函数
multiply(5); // 返回 10
multiply(5, 3); // 返回 15

greet 函数中,message 是一个可选参数,通过在参数名后加 ? 表示。而在 multiply 函数中,b 是一个默认参数,有默认值 2

函数重载

函数重载允许我们定义多个同名但参数列表不同的函数。TypeScript 会根据调用时传入的参数类型来决定调用哪个函数。

function formatValue(value: string): string;
function formatValue(value: number): string;
function formatValue(value: any): string {
    if (typeof value ==='string') {
        return value.toUpperCase();
    } else if (typeof value === 'number') {
        return value.toString();
    }
    return '';
}

let result1 = formatValue('hello'); // 返回 'HELLO'
let result2 = formatValue(123); // 返回 '123'

在上述代码中,我们定义了两个函数签名 formatValue(value: string): stringformatValue(value: number): string,然后实现了一个通用的 formatValue 函数。

接口(Interfaces)

接口是 TypeScript 中非常重要的概念,它用于定义对象的形状(shape),即对象应该包含哪些属性以及这些属性的类型。

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

let user: User = {
    name: 'Alice',
    age: 25
};

在上述代码中,我们定义了一个 User 接口,它要求对象必须包含 name(类型为 string)和 age(类型为 number)属性。然后我们创建了一个符合该接口的 user 对象。

可选属性与只读属性

接口中的属性可以是可选的,也可以是只读的。

interface Product {
    name: string;
    price: number;
    description?: string; // 可选属性
    readonly id: number; // 只读属性
}

let product: Product = {
    name: 'Book',
    price: 19.99,
    id: 123
};

// product.id = 456; // 报错,不能重新赋值只读属性

Product 接口中,description 是可选属性,通过在属性名后加 ? 表示。id 是只读属性,一旦赋值,不能再重新赋值。

接口继承

接口可以继承其他接口,从而扩展其功能。

interface Animal {
    name: string;
}

interface Dog extends Animal {
    breed: string;
}

let dog: Dog = {
    name: 'Buddy',
    breed: 'Golden Retriever'
};

在上述代码中,Dog 接口继承自 Animal 接口,因此 Dog 接口不仅包含 breed 属性,还必须包含 Animal 接口中的 name 属性。

类型别名(Type Aliases)

类型别名是为类型定义一个新的名字,它可以用于基本类型、联合类型、交叉类型等。

type Gender ='male' | 'female';

let person: {
    name: string;
    gender: Gender
} = {
    name: 'Bob',
    gender:'male'
};

在上述代码中,我们定义了一个 Gender 类型别名,它表示 'male''female' 两种值。然后我们使用这个类型别名来定义 person 对象的 gender 属性类型。

联合类型与交叉类型

联合类型表示一个值可以是多种类型中的一种,使用 | 分隔。交叉类型表示一个值同时具有多种类型的属性,使用 & 分隔。

// 联合类型
let value: string | number;
value = 'Hello';
value = 10;

// 交叉类型
interface A {
    a: string;
}

interface B {
    b: number;
}

let obj: A & B = {
    a: 'Hello',
    b: 10
};

在上述代码中,value 可以是 string 类型或 number 类型。而 obj 对象必须同时满足 A 接口和 B 接口的要求。

类(Classes)中的类型定义

在 TypeScript 中,类的使用与 JavaScript 类似,但可以增加类型定义。

class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet(): string {
        return `Hello, I'm ${this.name} and I'm ${this.age} years old.`;
    }
}

let person = new Person('Charlie', 30);
console.log(person.greet());

在上述 Person 类中,我们定义了 nameage 属性的类型,以及构造函数和 greet 方法的参数和返回值类型。

访问修饰符

TypeScript 支持三种访问修饰符:publicprivateprotected

  • public:默认修饰符,属性和方法可以在类内外访问。
  • private:属性和方法只能在类内部访问。
  • protected:属性和方法只能在类内部和子类中访问。
class BankAccount {
    private balance: number;

    constructor(initialBalance: number) {
        this.balance = initialBalance;
    }

    deposit(amount: number): void {
        this.balance += amount;
    }

    getBalance(): number {
        return this.balance;
    }
}

let account = new BankAccount(100);
// console.log(account.balance); // 报错,balance 是 private 属性
account.deposit(50);
console.log(account.getBalance()); // 输出 150

BankAccount 类中,balance 属性是 private 的,外部不能直接访问,只能通过类内部的方法来操作。

类的继承

TypeScript 中类可以继承其他类,子类会继承父类的属性和方法。

class Animal {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    speak(): string {
        return `${this.name} makes a sound.`;
    }
}

class Dog extends Animal {
    breed: string;

    constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }

    speak(): string {
        return `${this.name} barks.`;
    }
}

let dog = new Dog('Max', 'Labrador');
console.log(dog.speak());

在上述代码中,Dog 类继承自 Animal 类,并重写了 speak 方法。子类构造函数中必须调用 super 来初始化父类。

泛型(Generics)

泛型是 TypeScript 中一个强大的特性,它允许我们在定义函数、接口或类时使用类型参数,从而使代码更具复用性。

function identity<T>(arg: T): T {
    return arg;
}

let result = identity<number>(10);
let strResult = identity<string>('Hello');

在上述 identity 函数中,T 是一个类型参数。通过在函数名后使用 <T> 声明类型参数,我们可以在函数参数和返回值类型中使用它。这样,identity 函数可以接受任意类型的参数,并返回相同类型的值。

泛型接口与泛型类

我们也可以定义泛型接口和泛型类。

// 泛型接口
interface KeyValuePair<K, V> {
    key: K;
    value: V;
}

let pair: KeyValuePair<string, number> = {
    key: 'age',
    value: 30
};

// 泛型类
class Stack<T> {
    private items: T[] = [];

    push(item: T): void {
        this.items.push(item);
    }

    pop(): T | undefined {
        return this.items.pop();
    }
}

let stack = new Stack<number>();
stack.push(1);
stack.push(2);
console.log(stack.pop()); // 输出 2

KeyValuePair 接口中,我们使用了两个类型参数 KV 来表示键和值的类型。在 Stack 类中,T 表示栈中元素的类型。

转型过程中的工具与实践

逐步迁移

在从 JavaScript 转型到 TypeScript 时,建议采用逐步迁移的策略。可以先将一些核心的、稳定的 JavaScript 文件转换为 TypeScript 文件,然后逐步扩大范围。例如,先将工具函数、数据模型等文件转换为 TypeScript,确保这些部分的类型安全性。

使用 TypeScript 编译器选项

TypeScript 编译器提供了许多选项,可以根据项目需求进行配置。例如,strict 选项可以开启严格类型检查模式,能发现更多潜在的类型错误。在 tsconfig.json 文件中可以配置这些选项:

{
    "compilerOptions": {
        "strict": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true
    }
}

esModuleInteropallowSyntheticDefaultImports 选项可以帮助处理 ES6 模块和 CommonJS 模块之间的兼容性问题。

代码审查与类型检查

在转型过程中,加强代码审查非常重要。通过代码审查,可以确保团队成员正确使用 TypeScript 的类型系统,避免滥用 any 类型等问题。同时,利用 IDE 的类型检查功能,能在编码过程中及时发现类型错误,提高开发效率。

常见问题与解决方法

any 类型的滥用

过度使用 any 类型会使 TypeScript 的类型检查失去意义。如果遇到不确定类型的情况,可以先尝试使用更具体的类型,如 unknown,然后通过类型断言或类型守卫来确定具体类型。

let value: unknown = 'Hello';

if (typeof value ==='string') {
    let length = value.length; // 此时 value 被确定为 string 类型
}

类型兼容性问题

在 TypeScript 中,有时会遇到类型兼容性问题,特别是在使用第三方库时。例如,第三方库可能没有提供类型声明文件,或者类型声明文件与项目的 TypeScript 版本不兼容。这时可以使用 @types 社区提供的类型声明,或者自己编写类型声明文件。

与 JavaScript 的交互

在转型过程中,可能需要在 TypeScript 项目中使用一些 JavaScript 代码,或者反过来。TypeScript 支持与 JavaScript 的混合编程,但需要注意类型的传递和兼容性。例如,在 TypeScript 中调用 JavaScript 函数时,要确保参数和返回值的类型符合预期。

通过理解和掌握上述关键点,开发者能够更顺利地从 JavaScript 转型到 TypeScript,充分利用 TypeScript 的静态类型系统提升代码的质量和可维护性。在实际开发中,不断实践和总结经验,能更好地发挥 TypeScript 在前端开发中的优势。