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

TypeScript类型系统概述

2023-04-066.1k 阅读

什么是 TypeScript 类型系统

TypeScript 是 JavaScript 的超集,它为 JavaScript 添加了静态类型系统。类型系统在 TypeScript 中扮演着至关重要的角色,它允许开发者在编写代码时明确指定变量、函数参数和返回值的类型。这有助于在开发阶段发现许多潜在的错误,而不是等到代码在运行时出错。

例如,在传统的 JavaScript 中:

function add(a, b) {
    return a + b;
}
const result = add(5, 'hello');
console.log(result);

上述代码在运行时会出现问题,因为'hello'是字符串,不能直接与数字5相加。但在 JavaScript 中,只有运行时才能发现这个错误。

而在 TypeScript 中,我们可以这样写:

function add(a: number, b: number): number {
    return a + b;
}
const result = add(5, 'hello'); // 这里会报错,TypeScript 会提示类型不匹配
console.log(result);

TypeScript 的类型系统会在编译阶段就指出错误,提示'hello'的类型与参数b期望的number类型不匹配。

基础类型

布尔类型(boolean)

布尔类型是 TypeScript 中最基本的类型之一,它只有两个取值:truefalse

let isDone: boolean = false;

数字类型(number)

TypeScript 中的数字类型和 JavaScript 一样,都是双精度 64 位浮点值。它可以表示整数和小数。

let age: number = 25;
let pi: number = 3.14;

字符串类型(string)

字符串类型用于表示文本数据。可以使用单引号'或双引号"来定义字符串。

let name: string = 'John';
let message: string = "Hello, world!";

空值类型(void)

void类型通常用于表示函数没有返回值。

function logMessage(message: string): void {
    console.log(message);
}

未定义类型(undefined)和 null 类型

在 TypeScript 中,undefinednull各自有其类型。默认情况下,它们是所有类型的子类型。也就是说,undefinednull可以赋值给任何类型的变量。

let num: number | undefined;
num = undefined;

let str: string | null;
str = null;

但在严格模式下(strictNullChecks开启),undefinednull只能赋值给void和它们各自的类型。

// 开启 strictNullChecks
let num: number;
num = undefined; // 报错,不能将 undefined 赋值给 number 类型

任意类型(any)

any类型表示不特定的类型。当我们不确定一个变量的类型,或者我们想要允许在运行时动态地确定类型时,可以使用any

let value: any = 'hello';
value = 123;
value = true;

虽然any类型提供了很大的灵活性,但过度使用会削弱 TypeScript 类型系统的优势,因为它绕过了类型检查。

无值类型(never)

never类型表示永远不会有值的类型。通常用于函数永远不会返回(例如抛出异常或进入无限循环)的情况。

function throwError(message: string): never {
    throw new Error(message);
}

function infiniteLoop(): never {
    while (true) { }
}

复合类型

数组类型

在 TypeScript 中,可以使用两种方式定义数组类型。一种是在元素类型后面加上[],另一种是使用Array<元素类型>的形式。

let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ['a', 'b', 'c'];

还可以定义多维数组,例如二维数组:

let matrix: number[][] = [
    [1, 2],
    [3, 4]
];

元组类型

元组类型允许表示一个已知元素数量和类型的数组,但是各元素的类型不必相同。

let person: [string, number] = ['John', 25];

元组的元素访问和数组类似,但如果访问的索引超出了元组定义的范围,TypeScript 会报错。

let person: [string, number] = ['John', 25];
console.log(person[0]); // 输出 'John'
console.log(person[1]); // 输出 25
console.log(person[2]); // 报错,越界访问

对象类型

对象类型用于描述具有特定属性和方法的对象。可以使用接口(interface)或类型别名(type alias)来定义对象类型。 使用接口定义对象类型:

interface Point {
    x: number;
    y: number;
}
let point: Point = { x: 10, y: 20 };

使用类型别名定义对象类型:

type Point = {
    x: number;
    y: number;
};
let point: Point = { x: 10, y: 20 };

对象类型的属性可以是可选的,通过在属性名后面加上?来表示。

interface User {
    name: string;
    age?: number;
}
let user: User = { name: 'Alice' }; // 这里 age 是可选的

函数类型

在 TypeScript 中,函数类型由参数列表和返回值类型组成。可以使用类型别名或接口来定义函数类型。 使用类型别名定义函数类型:

type AddFunction = (a: number, b: number) => number;
let add: AddFunction = (a, b) => a + b;

使用接口定义函数类型:

interface AddFunction {
    (a: number, b: number): number;
}
let add: AddFunction = (a, b) => a + b;

函数的参数也可以是可选的,通过在参数名后面加上?来表示。

type GreetFunction = (name?: string) => string;
let greet: GreetFunction = (name) => name? `Hello, ${name}!` : 'Hello!';

类型推断

TypeScript 具有强大的类型推断能力,它可以根据变量的初始值或函数的返回值来推断出变量或函数的类型。

let num = 10; // TypeScript 推断 num 为 number 类型
function add(a, b) {
    return a + b;
}
let result = add(5, 3); // TypeScript 推断 result 为 number 类型,add 函数的参数为 number 类型

在函数返回值类型推断中,如果函数体内没有返回值,TypeScript 会推断返回值类型为void

function logMessage(message) {
    console.log(message);
}
// TypeScript 推断 logMessage 函数返回值类型为 void

但是在某些复杂情况下,类型推断可能无法准确推断出类型,这时就需要显式地指定类型。

let value;
if (Math.random() > 0.5) {
    value = 'hello';
} else {
    value = 123;
}
// 这里 TypeScript 无法准确推断 value 的类型,需要显式指定
let value: string | number;
if (Math.random() > 0.5) {
    value = 'hello';
} else {
    value = 123;
}

类型兼容性

TypeScript 的类型兼容性是基于结构子类型的。结构子类型意味着如果两个类型具有相同的结构,那么它们就是兼容的。

接口兼容性

对于接口类型,只要目标类型的属性是源类型属性的子集,那么源类型就可以赋值给目标类型。

interface Animal {
    name: string;
}
interface Dog extends Animal {
    breed: string;
}
let dog: Dog = { name: 'Buddy', breed: 'Golden Retriever' };
let animal: Animal = dog; // 这里 Dog 类型可以赋值给 Animal 类型,因为 Dog 包含 Animal 的所有属性

函数兼容性

在函数类型兼容性方面,参数的类型和数量以及返回值类型都需要考虑。 对于参数,源函数的参数类型必须与目标函数的参数类型兼容,并且源函数的参数数量不能多于目标函数的参数数量(除非目标函数的参数是可选的)。

type TargetFunction = (a: number, b: number) => void;
type SourceFunction = (a: number, b: number, c: number) => void;
let target: TargetFunction = (a, b) => console.log(a + b);
let source: SourceFunction = (a, b, c) => console.log(a + b + c);
target = source; // 报错,源函数参数数量多于目标函数

对于返回值类型,源函数的返回值类型必须与目标函数的返回值类型兼容。

type TargetFunction = () => number;
type SourceFunction = () => string;
let target: TargetFunction = () => 10;
let source: SourceFunction = () => 'hello';
target = source; // 报错,返回值类型不兼容

类型守卫

类型守卫是一种运行时检查机制,它允许开发者在运行时确定一个变量的类型。TypeScript 提供了几种类型守卫的方式。

typeof 类型守卫

typeof操作符可以用于类型守卫,它可以在运行时检查变量的类型。

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

instanceof 类型守卫

instanceof操作符用于检查一个对象是否是某个类的实例。

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;
    }
}
function greet(animal: Animal) {
    if (animal instanceof Dog) {
        console.log(`Hello, ${animal.name}, you are a ${animal.breed}`);
    } else {
        console.log(`Hello, ${animal.name}`);
    }
}

in 类型守卫

in操作符可以用于检查对象是否包含某个属性,从而进行类型守卫。

interface Bird {
    fly: () => void;
}
interface Fish {
    swim: () => void;
}
function move(animal: Bird | Fish) {
    if ('fly' in animal) {
        animal.fly();
    } else {
        animal.swim();
    }
}

泛型

泛型是 TypeScript 中非常强大的特性,它允许开发者在定义函数、接口或类时不指定具体的类型,而是在使用时再指定类型。

泛型函数

function identity<T>(arg: T): T {
    return arg;
}
let result = identity<string>('hello'); // 使用泛型函数并指定类型为 string
let numberResult = identity<number>(123); // 指定类型为 number

在泛型函数中,T是类型参数,它可以是任何有效的类型。在调用函数时,可以通过<类型>的方式指定具体的类型。

泛型接口

可以使用泛型来定义接口,使接口更加灵活。

interface GenericIdentityFn<T> {
    (arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;

泛型类

泛型也可以用于类的定义。

class GenericBox<T> {
    private value: T;
    constructor(value: T) {
        this.value = value;
    }
    getValue(): T {
        return this.value;
    }
}
let box = new GenericBox<string>('hello');
let value = box.getValue();

泛型类在创建实例时需要指定具体的类型。

交叉类型和联合类型

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型,这个类型包含了所有类型的特性。

interface A {
    a: string;
}
interface B {
    b: number;
}
let ab: A & B = { a: 'hello', b: 123 };

交叉类型常用于需要同时满足多个类型条件的场景,例如将多个接口的功能合并到一个对象上。

联合类型(Union Types)

联合类型表示一个值可以是多种类型之一。

let value: string | number;
value = 'hello';
value = 123;

在使用联合类型时,需要注意只能访问联合类型中所有类型共有的属性和方法。如果需要访问特定类型的属性和方法,可以使用类型守卫。

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

类型别名和接口的区别

定义方式

类型别名使用type关键字定义,而接口使用interface关键字定义。

type PointType = {
    x: number;
    y: number;
};
interface PointInterface {
    x: number;
    y: number;
}

功能差异

接口只能用于定义对象类型,而类型别名可以定义任何类型,包括基本类型、联合类型、交叉类型等。

type StringOrNumber = string | number;
type MyNumber = number;

接口可以通过继承来扩展,而类型别名不能继承,但可以通过交叉类型来实现类似的功能。

interface Shape {
    color: string;
}
interface Rectangle extends Shape {
    width: number;
    height: number;
}
type ShapeType = {
    color: string;
};
type RectangleType = ShapeType & {
    width: number;
    height: number;
};

在合并声明方面,接口支持合并声明,同名接口会自动合并属性,而类型别名不能合并声明。

interface User {
    name: string;
}
interface User {
    age: number;
}
// User 接口现在包含 name 和 age 属性
type AliasUser = {
    name: string;
};
// 这里不能再定义同名的 AliasUser 类型别名

高级类型

映射类型

映射类型允许我们基于现有的类型创建新的类型,通过对现有类型的属性进行变换。

interface User {
    name: string;
    age: number;
    email: string;
}
type ReadonlyUser = {
    readonly [P in keyof User]: User[P];
};
let readonlyUser: ReadonlyUser = { name: 'John', age: 25, email: 'john@example.com' };
// readonlyUser.name = 'Jane'; // 报错,不能修改只读属性

条件类型

条件类型允许我们根据条件选择不同的类型。

type IsString<T> = T extends string? true : false;
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false

索引类型

索引类型允许我们通过索引来访问对象的属性类型。

interface User {
    name: string;
    age: number;
}
type NameType = User['name']; // string
type AgeType = User['age']; // number

通过以上对 TypeScript 类型系统各个方面的详细介绍,开发者可以更好地利用 TypeScript 的类型系统来编写健壮、可维护的前端代码。无论是简单的基础类型,还是复杂的泛型、高级类型等,都为前端开发提供了强大的类型支持,帮助开发者在开发过程中减少错误,提高代码质量。