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

JavaScript与TypeScript的类型系统对比

2021-08-316.8k 阅读

JavaScript 类型系统概述

JavaScript 是一种动态类型、弱类型的编程语言。在 JavaScript 中,变量的类型在运行时确定,而且类型之间的转换相对宽松。

基本类型

JavaScript 有七种基本类型:stringnumberbooleannullundefinedsymbol(ES6 新增)和 bigint(ES2020 新增)。例如:

let name = 'John'; // string 类型
let age = 30; // number 类型
let isStudent = false; // boolean 类型
let n = null; // null 类型
let u; // undefined 类型
let sym = Symbol('unique'); // symbol 类型
let big = BigInt(9007199254740991); // bigint 类型

引用类型

除了基本类型,JavaScript 还有引用类型,比如 ObjectArrayFunction 等。引用类型的值存储在堆内存中,变量保存的是指向堆内存中实际对象的引用。

let obj = { key: 'value' }; // Object 类型
let arr = [1, 2, 3]; // Array 类型
function add(a, b) {
    return a + b;
} // Function 类型

类型自动转换

JavaScript 的弱类型特性使得类型自动转换非常常见。例如,当使用 + 运算符连接字符串和数字时:

let num = 5;
let str = '10';
let result = num + str; // 自动将 num 转换为字符串,结果为 '510'

在比较操作中也会发生类型转换:

console.log(5 == '5'); // true,因为 '5' 被转换为数字 5 进行比较

TypeScript 类型系统概述

TypeScript 是 JavaScript 的超集,它为 JavaScript 添加了静态类型系统。TypeScript 类型在编译时进行检查,可以在开发阶段发现类型错误,提高代码的可维护性和稳定性。

基本类型

TypeScript 支持与 JavaScript 相同的基本类型,并且语法上非常相似:

let name: string = 'John';
let age: number = 30;
let isStudent: boolean = false;
let n: null = null;
let u: undefined = undefined;
let sym: symbol = Symbol('unique');
let big: bigint = BigInt(9007199254740991);

类型注解

TypeScript 通过类型注解明确指定变量的类型。例如,对于函数参数和返回值也可以添加类型注解:

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

类型推断

TypeScript 具有类型推断能力,在很多情况下不需要显式地添加类型注解,TypeScript 可以根据变量的赋值自动推断其类型。

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

类型声明与推断对比

JavaScript 的动态类型声明

在 JavaScript 中,变量声明时不需要指定类型,变量的类型在运行时根据赋值确定。这使得代码编写非常灵活,但也容易在运行时出现类型相关的错误。

let value;
value = 'Hello';
value = 42; // 运行时不会报错,因为 JavaScript 是动态类型

TypeScript 的静态类型声明与推断

TypeScript 要求在声明变量时可以选择显式指定类型,或者通过类型推断确定类型。一旦类型确定,在编译时就会检查类型的一致性。

let value: string;
value = 'Hello';
// value = 42; // 编译错误,类型不匹配

在函数参数和返回值的类型声明上,TypeScript 可以通过类型注解明确函数的接口:

function greet(name: string): string {
    return 'Hello, ' + name;
}

而在 JavaScript 中,函数参数和返回值类型没有明确的声明,容易导致调用时传入错误类型的参数。

function greet(name) {
    return 'Hello, ' + name;
}
greet(42); // 运行时才可能发现错误

类型兼容性对比

JavaScript 的宽松类型兼容性

JavaScript 的类型兼容性非常宽松,因为它是弱类型语言。例如,函数调用时,实参的类型可以与形参期望的类型不一致,JavaScript 会尝试进行类型转换。

function printLength(str) {
    console.log(str.length);
}
printLength(123); // 运行时错误,因为 number 没有 length 属性,但 JavaScript 允许这样调用

TypeScript 的严格类型兼容性

TypeScript 在类型兼容性上更加严格,以确保类型安全。在函数调用时,实参的类型必须与形参的类型兼容。

function printLength(str: string) {
    console.log(str.length);
}
// printLength(123); // 编译错误,number 类型与 string 类型不兼容

在接口和类的继承与实现方面,TypeScript 也有严格的类型兼容性规则。例如,子类必须满足父类的类型要求:

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
class Dog extends Animal {
    breed: string;
    constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }
}

如果子类的属性或方法类型与父类不一致,TypeScript 会报编译错误,而 JavaScript 不会在语法层面进行这样的检查。

泛型的使用对比

JavaScript 没有泛型概念

JavaScript 本身没有泛型的概念。泛型是一种参数化类型的机制,允许代码在不指定具体类型的情况下编写可复用的组件。由于 JavaScript 是动态类型,不需要这种在编译时确定类型参数的机制。

TypeScript 的泛型

TypeScript 引入了泛型,使得代码可以在多种类型上复用,同时保持类型安全。例如,一个简单的泛型函数:

function identity<T>(arg: T): T {
    return arg;
}
let result = identity<number>(5); // result 类型为 number
let strResult = identity<string>('Hello'); // strResult 类型为 string

泛型在创建可复用的函数、类和接口时非常有用。比如,创建一个通用的数组操作类:

class ArrayUtils<T> {
    constructor(private arr: T[]) {}
    getFirst(): T | undefined {
        return this.arr.length > 0? this.arr[0] : undefined;
    }
}
let numArray = new ArrayUtils([1, 2, 3]);
let firstNum = numArray.getFirst(); // firstNum 类型为 number | undefined
let strArray = new ArrayUtils(['a', 'b', 'c']);
let firstStr = strArray.getFirst(); // firstStr 类型为 string | undefined

这种泛型机制在 JavaScript 中是无法实现的,通过 TypeScript 的泛型,代码可以在保证类型安全的前提下实现高度复用。

类型别名与接口对比

JavaScript 无类型别名与接口概念

JavaScript 没有类型别名和接口的概念。它通过对象字面量和原型链来定义对象的结构和行为,但没有一种机制可以像 TypeScript 那样在类型层面进行抽象和定义。

TypeScript 的类型别名

TypeScript 中的类型别名可以为任意类型定义一个新的名字。例如:

type UserId = number | string;
let userId: UserId = 123;
userId = '456';

类型别名可以用于联合类型、交叉类型等复杂类型的命名,提高代码的可读性。

TypeScript 的接口

接口主要用于定义对象的形状,即对象的属性和方法。

interface User {
    name: string;
    age: number;
    email: string;
}
function printUser(user: User) {
    console.log(`Name: ${user.name}, Age: ${user.age}, Email: ${user.email}`);
}
let myUser: User = {
    name: 'John',
    age: 30,
    email: 'john@example.com'
};
printUser(myUser);

接口和类型别名有一些区别。接口只能用于定义对象类型,而类型别名可以用于任何类型。接口可以重复声明并自动合并,而类型别名不行。例如:

interface User {
    phone: string;
}
// 这里的 User 接口会自动合并 phone 属性
interface User {
    address: string;
}
let user: User = {
    name: 'Jane',
    age: 25,
    email: 'jane@example.com',
    phone: '123-456-7890',
    address: '123 Main St'
};

而类型别名如果重复定义会报错:

type UserId = number | string;
// type UserId = boolean; // 报错,重复定义

枚举类型对比

JavaScript 无枚举类型

JavaScript 本身没有枚举类型。枚举是一种用于定义一组命名常量的类型。在 JavaScript 中,可以通过对象字面量模拟类似枚举的功能,但没有类型安全和编译时检查。

const Colors = {
    RED: 0,
    GREEN: 1,
    BLUE: 2
};
let color = Colors.RED;
// color = 'yellow'; // 运行时不会报错,因为没有类型检查

TypeScript 的枚举类型

TypeScript 提供了枚举类型,分为数字枚举和字符串枚举。数字枚举:

enum Colors {
    RED = 0,
    GREEN = 1,
    BLUE = 2
}
let color: Colors = Colors.RED;
// color = 'yellow'; // 编译错误,类型不匹配

字符串枚举:

enum Directions {
    NORTH = 'north',
    SOUTH ='south',
    EAST = 'east',
    WEST = 'west'
}
let direction: Directions = Directions.NORTH;

枚举类型在 TypeScript 中提供了类型安全和代码可读性的提升,而 JavaScript 中需要手动模拟且无法在编译时进行类型检查。

类型保护对比

JavaScript 无类型保护机制

JavaScript 没有专门的类型保护机制。在运行时,需要通过 typeofinstanceof 等操作符来检查值的类型,但这些操作不会在类型层面进行保护。

function printValue(value) {
    if (typeof value ==='string') {
        console.log(value.length);
    } else if (typeof value === 'number') {
        console.log(value.toFixed(2));
    }
}
printValue('Hello');
printValue(123);

这里 typeof 只是在运行时检查类型,无法在代码的其他部分保证类型的一致性。

TypeScript 的类型保护

TypeScript 提供了类型保护机制,通过 typeofinstanceof 等操作符可以在特定代码块内细化类型。例如:

function printValue(value: string | number) {
    if (typeof value ==='string') {
        console.log(value.length); // 这里 value 被细化为 string 类型
    } else if (typeof value === 'number') {
        console.log(value.toFixed(2)); // 这里 value 被细化为 number 类型
    }
}
let strValue: string | number = 'Hello';
printValue(strValue);

此外,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));
    }
}
let numValue: string | number = 123;
printValue(numValue);

这种类型保护机制使得 TypeScript 在处理联合类型时更加安全和灵活,而 JavaScript 缺乏这种编译时的类型细化和保护能力。

总结

通过以上对 JavaScript 和 TypeScript 类型系统的详细对比,可以看出 JavaScript 的类型系统灵活但缺乏编译时的类型检查,容易在运行时出现类型错误。而 TypeScript 的静态类型系统为 JavaScript 带来了类型安全、代码可维护性和可读性的提升,尤其在大型项目中,TypeScript 的优势更加明显。开发人员可以根据项目的规模、需求和团队技术栈来选择使用 JavaScript 或 TypeScript,以达到最佳的开发效率和代码质量。