侧边栏

学习Typescript

发布于 | 分类于 编程语言/TypeScript

最近用TypeScript写了一个游戏,顺道把博客的后端项目也改成了ts版本,下面整理在学习TypeScript时遇见的的问题。

参考文档

开发环境

安装

首先全局安装tsc

npm install -g typescript

然后就可以编写typescript文件并将其编译成javascript文件,然后执行了

bash
# 创建test.ts文件并写入内容
touch test.ts && echo 'let a:number = 100; console.log(a);' > test.ts
# 编译ts文件
tsc test.ts
# 在当前目录下生成test.js
node test.js

vscode对typescript的支持十分友好(毕竟vscode就是ts写的),因此建议使用vscodewebStorm作为ts开发工具。

tsconfig

除了上面指定tsc filename.ts的基础调用方式之外,还可以通过额外的命令行参数,如输入文件、输出目录等控制编译

bash
tsc index.ts --相关参数

将编译配置参数都通过命令行的形式传入是一件比较麻烦的事,因此ts提供了一个叫tsconfig.json的配置文件,用来指定ts编译的一些参数信息,包括用来编译这个项目的根文件和编译选项。

可以通过下面命令快速创建tsconfig.json文件

bash
tsc --init

如果一个目录下包含tsconfig.json文件,那么该目录将会作为这个ts项目的根目录。

tsconfig中有很多配置项,具体字段可以参考官方文档

这些配置项大多数跟工程相关,并不是学习TS语法必须的,初学时可以直接跳过。

配置编辑器

如果每次修改ts文件都需要tsc编译之后再通过NodeJS再运行,则比较繁琐,不方便快速调试代码。

这种情况下,可以使用ts-node直接运行ts代码

bash
npm install -g ts-node
# 直接执行ts文件
ts-node test.ts

只需要一个命令就可以运行ts文件,这在初学TS时非常有用。

除了命令行运行,还可以让代码编辑器也支持直接运行TS代码,这样连命令行也不需要键入了。

下面演示了如何配置webstrom直接运行ts代码。

首先还是按照上面的方式全局安装ts-node

然后安装Run Configuration for TypeScript这个webstrom插件。

之后再使用webstrom打开ts文件,右上角的运行按钮就变成绿色了,点击即可运行和调试了。

此外还可以在tsconfig.json中对ts-node进行一些配置,配置文档

json
{
  "compilerOptions": {},
  "ts-node": {
    "transpileOnly": true,
    "compilerOptions": {
      "module": "esnext",
      "esModuleInterop": true,
      "resolveJsonModule": true,
      "moduleResolution": "node",
    },
    "esm": true
  },
}

具体的配置参数这里不再展开。

了解了开发环境的搭建后,接下来学习TypeScript的基础语法。

由于TS本身是JS的超集,可以直接运行JS代码,因此这里不会介绍JS本身的语法,如定义变量、声明函数、条件判断、循环、闭包等,而是关注TS独有的类型系统相关的语法。

基础语法

TypeScript里的类型注解是一种轻量级的为函数或变量添加约束的方式。其格式为

variableName: variableType

首先需要明确的是,ts中存在两种声明空间:类型声明空间与变量声明空间

  • 类型声明空间包含用来当做类型注解的内容,如 class XXXinterface XXXtype XXX
  • 变量声明空间包含可用作变量的内容,如let iconst j

下面整理了ts中的变量类型,包括基础类型和可自定义类型的一些写法,建议直接阅读官方文档

基础类型

原始类型

TS在类型声明空间中,内置了一些基础的数据类型

  • boolean,布尔值

  • number,ts所有数字都是浮点数

  • string,与js相同,支持模板字符串

  • 数组:

    • 基础类型[],例如number[]
    • 数组泛型,例如Array<number>
  • 元组[string, number],需要保证对应位置的元素类型保持一致

  • enum

  • ts
      enum Color {Red = 1, Green = 2, Blue = 4}
      let c: Color = Color.Green;
  • any,主要用于在编程阶段还不清楚类型的变量指定一个类型,使用any可以直接让这些变量通过编译阶段的检查

  • void,与any相反,表示没有任何类型,通常声明函数没有任何返回值

  • nullundefined,在--strictNullChecks模式下,nullundefined只能赋值给void和它们各自

  • never,表示的是那些永不存在的值的类型

非原始类型

除了上面的基本类型之外的所有类型,都被称为object,表示非原始类型。

在某些时候,我们需要主动确定某个变量的类型,此时可以通过类型断言告诉ts编译器

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

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

类型断言可以理解为强制类型转换。

接口

TypeScript的核心原则之一是对值所具有的结构进行类型检查,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

声明变量的类型

传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配,可以使用interface接口来自定义变量的类型

ts
// interface定义接口
interface Person {
    name: string; //定义属性名name,对应数据类型为string,不包含该属性会报错
    age?: number; // 可选属性,如果不传也不会报错
    readonly from: string; // 只读属性
    [prop: string]: any; // 允许包含其他数据类型为any属性,去掉则无法传递avatar参数
}
// 指定了greet函数的参数类型为Person
function greet(p: Person):void {
    console.log(p.name);
    // p.from = '123'; 无法修改只读属性
	  console.log(p.xxx); // 因为声明了[prop: string]: any,导致此处不会报错

    if (p.age) {
        console.log(p.age);
    }
}
// 此处就会对参数类型进行检测
greet({ name: "shymean", from:"chengdu", age: 18, avatar: "http://xxx/xx.jpg" });

类型检测限制了变量的类型,而可选属性的好处有

  • 可以对可能存在的属性进行预定义,放宽了对于变量的属性检测限制
  • 相比较使用[prop: string]: any,可以捕获引用了不存在的属性时的错误

声明函数的签名

函数的签名包括了参数和返回值类型,由于JavaScript中函数可以通过函数表达式进行声明,因此在ts中,接口除了描述自定义变量的类型,也可以用来描述函数的类型

ts
interface greetFunc {
  (person: Person): void;
}
let greet: greetFunc = function greet(p: Person) {};
// 调用方式同上
greet({ name: "shymean", from:"chengdu", age: 18, avatar: "http://xxx/xx.jpg" });

类的接口

接口也可以用来限定某个类的实现

ts
interface ClockInterface {
    currentTime: Date;
  	showTime(time: Date):void
}
// 类需要实现接口的属性和方法
class Clock implements ClockInterface {
    currentTime: Date; // 如果不声明ClockInterface接口上的属性,则会提示错误
    constructor(h: number, m: number) { }
    showTime(){}
}

需要注意的是:当一个类实现了一个接口时,只对其实例部分进行类型检查,类的静态部分不再检查范围内。

其他

接口也可以互相继承,组合成新的接口,这样可以在多个接口间复用公共的属性

class是一种比较常见的自定义类型,注意class Person除了在类型声明空间提供了一个Person的类型,也在变量声明空间提供了一个可以使用的变量Person用于实例化对象

下面是一个简单的例子

ts

class Person {
    // 增加访问修饰符
    private age: number; // 只能在当前类中访问
    protected name: string; // 只能在当前类及其子类访问
    gender: number; // 默认public,可在实例上访问

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

    // 方法也可以使用访问修饰符
    public show() {
        console.log(`${this.name}:${this.age} ${this.gender}`);
    }
}

let p: Person = new Person("shymean", 10, 1);
console.log(p.gender);
// console.log(p.name); // 访问protected属性会报错
// console.log(p.age); // 访问private属性会报错
p.show();

// 继承
class Student extends Person {
    static count: number = 0;
    readonly grade: string = undefined; // 只读属性
    constructor(name: string, age: number, gender: number, grade: string) {
        // 构造函数里访问 this的属性之前,一定先要调用 super()
        super(name, age, gender);
        this.grade = grade // 只读属性只能在声明时或者构造函数中初始化

        // 静态属性
        Student.count++;
    }
  	// 重写父类的show方法
    show(){
        // 可以访问父类的public和protected属性
        console.log(`${this.name} ${this.gender}`);
        // 无法访问父类的私有属性
        // console.log(this.age);
        
        // 调用父类方法
        super.show(); 
    }
    // 静态方法
    static getCount() {
        // this.show() // 静态方法类无法调用实例方法
        console.log(Student.count)
    }
}

let s = new Student("shymean", 10, 1, "freshman");
s.show()

let s2 = new Student("shymean2", 10, 1, "freshman");

Student.getCount() // 返回 2

function test(p:Person){
    p.show()
}

test(p);
test(s); // 子类也可以通过类型检测

类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型

ts
type User = {
    name: string,
    age: number
}
type Name = string;

类型别名与接口的区别在于

  • 接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名
  • 类型别名不能像接口一样被继承和实现

泛型

为了扩展函数的可复用性,接口不仅要能够支持当前的数据类型,也需要能够支持未来的数据类型,这就是泛型的概念。

也就是说,我们可以在编写代码时(如调用方法、实例化对象)指定数据类型。

泛型变量

一个典型的例子是:函数需要返回与参数类型相同的值

ts
// T可以看做是正则表达式里面的捕获,获取了参数的类型后,就可以用来声明返回值的类型了
function identity<T>(arg: T): T {
    return arg;
}
let a: number = identity<number>(2)
let b: string = identity<number>(2) // 报错:无法将number类型赋值给string类型
let c: string = identity<string>("hello"); // 传入不同的T类型

泛型接口

在上面的例子中,可以通过泛型接口来指定泛型类型

ts
interface identityFunc {
    <T>(arg: T): T
}

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

泛型类

泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。

ts
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

// 实现一个数字的add方法
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) {
    return x + y;
};
let a: number = myGenericNumber.add(10, 20);

// 实现一个字符串的add方法
let myGenericString = new GenericNumber<string>();
myGenericString.zeroValue = '';
myGenericString.add = function(x, y) {
    return x + y;
};
let d: string = myGenericString.add("hello", "world");

泛型约束

在某些时候需要指定实现某些特定的数据类型

ts
interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  
    return arg;
}
loggingIdentity(1) // T需要实现 Lengthwise,即包含length属性

模块和高级语法

为了实现各种校验的需求,TS的类型系统也可以进行编程,即在类型中进行条件判断、递归等操作,也被称作类型体操。

类型编程是TS中比较复杂和有趣的内容,在后面的文章单独整理,这里不单独展开。

由于TS和JS可能混用,且为了兼容JS庞大的模块生态,TS的模块是另外一个有些复杂的知识点,后面单独整理

从JavaScript项目迁移

*.d.ts声明文件

由于ts增加了类型声明,使用变量前需要先进行声明,这导致在调用很多原生接口(浏览器、Node.js)或者第三方模块的时候,因为变量未声明而导致编译器的类型检查失败。

由于目前主流的库都是通过JavaScript编写的,且用 ts 写的模块在发布的时候仍然是用 js 发布,因此手动修改原生接口或者第三方模块源码肯定是不现实的,如何对原有的JS库也提供类型推断呢?

typescript先后提出了 tsd(已废弃)、typings(已废弃)等功能,最后提出了 DefinitelyTyped,该规范需要用户编写.d.ts类型定义文件,用来给编译器以及IDE识别是否符合API定义类型。

下面是一个描述d.ts文件作用的例子。假设有一个之前编写的js工具库util.js

js
// util.js
module.exports = {
    log(msg) {
        console.log("util.log: ", msg);
    }
};

在新的ts项目中,我们需要使用这个工具库

ts
// a.ts
import util = require('./util.js') // 由于util模块使用commonjs规范,使用import = require语法导入模块

// 此处编辑器不会帮我们做任何提示
util.log("msg")
util.log() // 不知道参数的个数、类型等信息

由于util是js文件,我们无法使用ts的类型推断功能,也不知道util.log方法的参数类型和返回值类型。接下来让我们编写util.d.ts来帮助编辑器和ts编译器

ts
// util.d.ts
declare var util: {
    test(msg: string): void;
};

export = util;

此时查看a.ts中的代码(可能需要重启下vscode),就可以看见下面的错误提示

这样,我们就完成了在ts项目中为js库增加类型推断的功能。

上面的例子展示了在现有ts项目中引入js模块,并增加类型检测的方法。如果想要了解更多关于d.ts的内容,可以参考

另外上面这个例子也从侧面展示了将现有js项目迁移到ts的方式。一般来说,将现有JavaScript项目迁移到TypeScript项目是十分简单的,由于任何JS文件都是有效的TS文件,因此最简单的迁移流程应该是

  • 添加一个 tsconfig.json 文件,方便配置项目和编译相关信息
  • 把文件扩展名从 .js 改成 .ts,开始使用 any 来减少错误;
  • 开始在 TypeScript 中写代码,尽可能的减少 any 的使用;
  • 回到旧代码,开始添加类型注解,并修复已识别的错误;
  • 手动编写d.ts文件,为第三方 JavaScript 代码定义环境声明。

开发node服务

参考

有了ts-node,搭建node服务开发环境就变得比较简单了,结合nodemon,还可以实现文件热更新等功能。

export NODE_ENV=development && nodemon --watch 'server/**/*' -e ts,tsx --exec ts-node ./server/index.ts

需要安装@types/node包,然后再tsconfig.json中配置

npm i @types/node -D
json
{
    "compilerOptions": {
        "types": [
            "node"
        ]
    }
}

开发前端应用

vue

vue源码中使用flow作为类型检测机制,在正在开发的vue3版本中,计划改用typescript,因此在学习typescript并在vue开发中ts就变得理所应当,参考TypeScript 支持-Vue文档

react

根据描述,typescript支持内嵌、类型检查以及将JSX直接编译为js文件,因此在react中使用ts是十分方便的。参考TypeScript 中文手册-React

小结

Typescript有下面几个优点

  • 在开发阶段,如果参数类型不正确,或者调用了不存在的方法,就会在编译阶段抛出错误,减少潜在的bug
  • 强类型的变量和参数,允许IDE提供代码智能提示,也方便代码阅读

本文主要整理了在学习Typescript过程中的一些笔记

  • 介绍了ts开发环境的安装,使用ts-node快速运行ts代码
  • 整理了ts中的基本类型(number、boolean、enum等)和自定义类型(接口、类、类型别名)相关语法
  • 整理了ts中泛型和模块的相关概念
  • .d.ts出发,了解了从JavaScript项目迁移到TypeScript的大致流程

当然还遗漏了很多细节语法等问题,需要在项目使用中进一步学习。最后放上一个问题:弱类型、强类型、动态类型、静态类型语言的区别是什么?

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。