一文搞定TypeScript

261 阅读10分钟

TypeScript 是 JavaScript 的静态类型检查系统,它是一门强类型语言,弥补了 JS 弱类型的缺陷。

TS是怎么把JS变成强类型的呢?通过以下这些手段:

  1. 扩展 JS 的数据类型。
  2. 类的增强,增加修饰符系统和装饰器,前者用来约束类成员的行为或访问权限,后者是一种特殊类型的声明,用于附加或修改类、类成员(方法、属性、访问器)或参数的行为。
  3. 提供方便类型检测的特性,如类型注解、类型推论、类型断言、类型保护、类型兼容性、类型导入等。
  4. 提供编译器配置选项,根据配置编译出不同的 JS 代码。
  5. 提供声明文件(以.d.ts结尾的文件),用于为JS代码提供类型声明。

一 数据类型

本文对TS的类型和简单使用做概括,一些简单的JS原生类型不做说明。

  • string

    let name: string = "bob";

  • number

    let decLiteral: number = 6;

  • boolean

    let isDone: boolean = false;

  • bigint

    const oneHundred: bigint = BigInt(100);

  • symbol

    const sym(: symbol) = Symbol("key");

  • null

    默认情况下null和undefined是所有类型的子类型,就是说你可以把null和undefined赋值给其它类型的变量。

    let n: null = null;

  • undefined

    let u: undefined = undefined;

  • object

    object表示非原始类型,也就是除number,string,boolean,symbol,bigint,null或undefined之外的类型。

    let obj: object = { name: "Alice" };

  • never

    表示的是那些永不存在的值的类型,never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。即使 any也不可以赋值给never。

1.存在无法达到的终点 
  function error(message: string): never {
      throw new Error(message);
  }
  function infiniteLoop(): never {
      while (true) {}
  }

2.推断的返回值类型为never 
  function fail() {
      return error("Something failed");
  }
  • any

    可以表示任何类型的值。

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay
  • void

    表示没有任何类型,当一个函数没有返回值时,你通常会见到其返回值类型是void(js中函数没有返回值时返回undefined,但TS做了更严格的语义上的区分)。

function warnUser(): void {
    console.log("This is my warning message");
}
  • unknown

    类似 any,但更安全,操作前需进行类型检查。

    let userInput: unknown;

  • 函数类型

    函数类型包含两部分:参数类型和返回值类型。

    JS有五种函数类型:普通函数、异步函数、生成器函数、访问器属性、静态方法。

    本文只介绍普通函数在TS中的运用,其它的函数类型使用非常自然,一样的对参数和返回值约束即可。

    普通函数类型有三种表达方式:function声明、function函数表达式、箭头函数表达式

    函数类型的注解有两种位置写法,一种是写在变量声明处,另一种是function声明或函数表达式的内部。

let add: (a: number, b: number) => number; 【类型注解写在变量声明处】
const func: (a: number, b: number) => number = (a, b) => a + b;【类型注解写在变量声明处】
function add(a: number, b: number): number { return a + b; } 【类型注解写在function声明内部】
const add = function add(a: number, b: number): number { return a + b; } 【类型注解写在function函数表达式内部】
const add = (a: number, b: number): number => a + b; 【类型注解写在箭头函数表达式内部】

函数重载:为同一个函数提供多个类型定义叫函数重载,可以对函数调用时的参数和返回值起到约束效果。

// 函数重载声明
function reverse(input: string): string;
function reverse<T>(input: T[]): T[];
// 函数实现
function reverse(input: any): any {
    if (typeof input === "string") {
        return input.split("").reverse().join("");
    } else if (Array.isArray(input)) {
        return [...input].reverse(); // 创建新数组避免修改原数组
    }
}
// 使用示例
const str = reverse("hello");   // 类型为 string
const arr = reverse([1, 2, 3]);  // 类型为 number[]
console.log(str); // "olleh"
console.log(arr); // [3, 2, 1]

可选参数:在TS里我们可以在参数名旁使用?实现可选参数的功能。

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
    }
let result1 = buildName("Bob");  // works correctly now
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams");  // ah, just right
  • 数组类型

    数组的两种注解方式:

    • let list: number[] = [1, 2, 3];
    • let list: Array<number> = [1, 2, 3];
  • 元组类型

    表示固定长度和类型的数组,各元素类型可不同。

    let x: [string, number] = ['abc',123];

  • 字面量类型

    约束变量为特定值。

    type Easing = "ease-in" | "ease-out" | "ease-in-out";

  • 类型别名

    定义自定义类型。

    type StringOrNumber = string | number;

  • 枚举类型

    为一组值提供命名常量。

    enum Color { Red=1, Green, Blue };

  • 类类型

    TS对类做了很多加强改造,使JS可以像Java一样非常丝滑的使用面向对象思维进行编程,一个关于TS类的例子,涉及全面的知识点:

// 1. 抽象类与继承
abstract class Vehicle {

    // 2. 访问修饰符(public protected private)
    private readonly VIN: string;  // 3. readonly修饰符
    
    constructor(
        public brand: string,   // 4. 参数属性简写
        protected model: string,
        private _productionYear: number
    ) {
        this.VIN = this.generateVIN();
    }
    
    // 5. 抽象方法
    abstract getVehicleInfo(): string;

    // 6. 静态方法
    static createDefaultVehicle() {
        return new Car('Toyota', 'Camry', 2023, 4);
    }

    private generateVIN(): string {
        return `VIN-${Math.random().toString(36).slice(2, 10)}`;
    }
}

// 7. 接口实现
interface Serializable {
    serialize(): string;
}

// 8. 继承抽象类
class Car extends Vehicle implements Serializable {

    // 9. 静态属性
    static carTypes: string[] = ['Sedan', 'SUV', 'Coupe'];

    constructor(
        brand: string,
        model: string,
        productionYear: number,
        private doors: number
    ) {
        super(brand, model, productionYear);
    }

    // 10. 实现抽象方法
    getVehicleInfo(): string {
        return `${this.brand} ${this.model} (${this['_productionYear']})`; 
    }

    // 11. 方法重写
    override getVehicleInfo(): string {
        return super.getVehicleInfo() + ` with ${this.doors} doors`;
    }

    // 12. 存取器(getter/setter)
    get productionYear(): number {
        return this['_productionYear'];
    }

    set productionYear(year: number) {
        if(year > 2000) {
            this['_productionYear'] = year;
        }
    }

    // 13. 实现接口方法
    serialize(): string {
        return JSON.stringify({
            brand: this.brand,
            model: this.model,
            year: this.productionYear,
            doors: this.doors
        });
    }

    // 14. 方法重载
    honk(times: number): void;
    hont(timeMs: number): void;
    honk(input: number | string): void {
        // 实现逻辑
    }
}

// 15. 装饰器,如何约束一个变量为类
function LogClass(target: new () => object) {
    console.log(`Class ${target.name} created`);
}

@LogClass
class ElectricCar extends Car {

    constructor(
        brand: string,
        model: string,
        productionYear: number,
        doors: number,
        private batteryCapacity: number
    ) {
        super(brand, model, productionYear, doors);
    }
    
    // 16. 方法覆盖
    override getVehicleInfo(): string {
        return `${super.getVehicleInfo()} Battery: ${this.batteryCapacity}kWh`;
    }
}

// 使用示例
const myCar = new Car('Honda', 'Accord', 2023, 4);
console.log(myCar.getVehicleInfo()); // "Honda Accord (2023) with 4 doors"
console.log(Car.carTypes);          // 访问静态属性
console.log(Vehicle.createDefaultVehicle()); // 调用静态方法
const electric = new ElectricCar('Tesla', 'Model S', 2023, 2, 100);
console.log(electric.serialize());
  • 接口类型

    用来约束类、对象、函数的规范,接口和类型别名很相似,最大的区别是约束类上的不同。

    约束类:表达了某个类是否拥有某种能力。类可以用来实现某个接口,于是这个类就被接口约束了。

    约束对象:interface LabelledValue { label: string; }

    约束函数:interface AddInterface { (a: number, b: number): number; },大括号只是一个定界符而已

  • 联合类型

    可以是多种类型之一。

    let a:string | number;

  • 交叉类型

    合并多个类型为一个类型。

// 定义两种类型
interface Person {
    name: string;
    age: number;
}

interface Employee {
    employeeId: string;
    department: string;
}

// 创建交叉类型
type EmployeePerson = Person & Employee;

// 使用交叉类型(必须包含所有属性)
const employee: EmployeePerson = {
    name: "Alice",
    age: 30,
    employeeId: "E12345",
    department: "IT"
};

console.log(employee.name);         // Alice
console.log(employee.employeeId);   // E12345
  • 泛型

    附属于函数、类、接口、类型别名之上的类型,相当于是一个类型变量,在定义时无法预先知道具体的类型,只有到调用时,才能确定它的类型。

    在函数名、类名、接口名、类型别名后通过尖括号<>传入泛型。

    使用extends关键字,可用于对泛型的取值进行约束。

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  
    return arg;
}

二 类的增强

修饰符

  • public:可以自由的访问成员。

  • private:约束成员不能在声明它的类的外部访问。

  • protected:受保护的成员,只能在自身和子类中访问。

  • readonly:约束成员为只读,这里可以是类、类型别名、接口等的成员。

  • abstract:定义抽象类、抽象成员。

  • static:JS中仅是标记静态成员的语法,在TS中是明确的类成员修饰符,用于控制成员属于类本身而非实例。

装饰器

为某些类、类的成员(属性、方法、方法参数)提供元数据信息(描述数据的数据),其本质是函数。

// 类装饰器
function ClassDecorator(constructor: Function) {
  console.log(`类装饰器应用于类: ${constructor.name}`);
  // 添加元数据或扩展类
}

// 属性装饰器
function PropertyDecorator(target: any, propertyKey: string) {
  console.log(`属性装饰器应用于属性: ${propertyKey}`);
  // 跟踪属性访问/修改
}

// 方法装饰器
function MethodDecorator(
  target: any,
  methodName: string,
  descriptor: PropertyDescriptor
) {
  console.log(`方法装饰器应用于方法: ${methodName}`);
  
  // 可以修改方法行为(如添加日志)
  const original = descriptor.value;
  
  descriptor.value = function (...args: any[]) {
    console.log(`${methodName} 被调用`);
    return original.apply(this, args);
  };
}

// 参数装饰器
function ParameterDecorator(
  target: any,
  methodName: string,
  parameterIndex: number
) {
  console.log(`参数装饰器应用于方法 ${methodName} 的第 ${parameterIndex + 1} 个参数`);
  // 记录参数元数据
}

@ClassDecorator
class ExampleClass {

  @PropertyDecorator
  public sampleProperty: string = "Hello";

  @MethodDecorator
  public greet(@ParameterDecorator name: string): void {
    console.log(`Welcome, ${name}!`);
  }
}

// 测试代码
const example = new ExampleClass();         // 触发类装饰器
console.log(example.sampleProperty);       // 触发属性装饰器(访问)
example.greet("TypeScript");               // 触发方法和参数装饰器

各装饰器触发顺序:
参数装饰器(当类被加载时)
方法装饰器
属性装饰器
类装饰器

输出结果:
参数装饰器应用于方法 greet 的第 1 个参数
方法装饰器应用于方法: greet
属性装饰器应用于属性: sampleProperty
类装饰器应用于类: ExampleClass
Hello
greet 被调用
Welcome, TypeScript!

三 类型特性

  • 类型注解

    一种轻量级的为函数或变量添加约束的方式,使用方式是用冒号+类型的方式。

  • 类型推论

    在有些没有明确指出类型的地方,编译器会根据值自动地确定类型。

  • 类型演算

    根据已知的信息,计算出新的类型,TS为我们提供了以下三种方式:

    typeof:书写在类型约束的位置上,表示获取某个数据的类型,当它作用于类的时候,得到的类型是该类的构造函数。

    keyof:作用于类、接口、类型别名,用于获取其他类型中的所有成员名组成的联合类型。

    in:往往和keyof连用,限制某个索引类型的取值范围。

  • 类型断言

    在你清楚地知道一个实体具有比它现有类型更确切的类型时使用。

    类型断言有两种形式:

    1.尖括号

    let someValue: any = "this is a string";

    let strLength: number = (<string>someValue).length;

    2.as语法

    let someValue: any = "this is a string";

    let strLength: number = (someValue as string).length;

  • 类型保护

    当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型,触发类型保护的方式有:typeof、instanceof、in、自定义类型谓词等。

  • 类型兼容性

    类型赋值时,如何判定它们是否兼容?

    对基本类型而言,要求完全匹配;对对象而言,考虑到JS的实际情况(比如JS广泛地使用匿名对象),TS提出用鸭子辩型法(子结构辩型法)进行判断,即判断某个事物是不是鸭子,只用判断它是否具有鸭子的特征即可。

  • 类型导入

    TS使用了两个概念扩展了import语法,用于声明类型的导入。

    • import type:一个只能导入类型的导入语句。
    • 内联type导入:在单个导入中添加前缀type,以指示导入的引用是一种类型。

四 编译器配置选项

TS所有编译配置选项文档链接:www.tslang.cn/docs/handbo…

五 声明文件

以.d.ts结尾的文件,用于为js代码提供类型声明,声明文件可以放在项目的这些位置:

(1)放置到tsconfig.json配置中包含(includes选项)的目录

(2)放置到node_modules/@types文件夹中

(3)根据typeRoots选项手动配置(使用这种方式,前面两种方式将失效)

(4)与JS代码所在目录相同,并且文件名也相同的文件 最佳

如果代码是TS写的,可以用declaration配置的方式自动生成声明文件,如果代码是JS写的需要手动编写声明文件。编写声明文件新用到的语法关键字有:三斜线指令、declare、namespace、module。

三斜线指令:包含单个XML标签的单行注释,注释的内容会作为编译器指令使用。

declare:为 TS 编译器提供类型声明,告诉编译器某个实体(变量、函数、类、模块等)已经存在,并在编译阶段提供其类型信息,但不生成任何实际的 JavaScript 代码。

namespace:表示命名空间,可以将其认为是一个对象,命名空间中的内容,必须通过命名空间.成员名的方式访问。

module:为第三方JS库或无类型的模块提供完整的类型定义。

总结:

JS最为一门弱类型语言,不利于编写大型应用,但是TS从根本上解决了这一问题,它提供了一套新的类型系统。

同时它也是可选的,也就是说用户可以自由的选择性地使用,并且能够按照自己的需求配置编译选项。