9

一篇文章学会 TypeScript (内部分享标题:TypeScript 基础)

这篇文章是我在公司前端小组内部的演讲分享稿,目的是教会大家使用 TypeScript,
这篇文章虽然标着基础,但我指的基础是学完后就能够胜任 TypeScript 的开发工作。
从我分享完的效果来看,大家都学会了,效果还是好的。我把这篇分享稿发布出来,
希望能对大家的 TypeScript 学习有所帮助。

1、什么是 TypeScript?

TypeScript 是由微软发布的一款开源的编程语言。它是 JavaScript 的超集(兼容 JavaScript 代码),其代码必须经过编译后,方可在 JavaScript 环境中运行,核心功能是类型系统可以提前使用 ES 的新特性

接下来来看一段代码示例:

// TypeScript 语法
function add(x: number, y: number): number {
    const res: number = x + y;
    return res;
}

// 与 C 语言比较
int add(int x, int y) {
    int res = x + y;
    return res;
}

当类型不对的时候,IDE 会提示错误:
clipboard.png

编译后:

// JavaScript 语法
function add(x, y) {
    const res = x + y;
    return res;
}

联想:大致可以把它看成是加了类型系统的 Babel。

问题:为什么要学习 TypeScript?

  1. 类型系统可以在代码编译阶段就可以发现潜在的变量类型问题,甚至在开发阶段,IDE 就可以帮助我们找到这些潜在的问题,避免了项目在上线后才发现变量类型不对的错误。
  2. TypeScript 是前端开发的趋势,有些特性还可能会成为 ES 的新标准,Vue 3.0 也会采用 TypeScript 作为开发语言,当你想看源码的时候不至于看不懂,跟着趋势走肯定是没错的。
  3. 减少类型校验等无用的代码书写。
  4. 可以提前享受 ES 新特性带来的便利。
  5. 向 qian 看,qian 途。

2、快速上手,三步走

第一步:全局安装 TypeScript

$ yarn global add typescript

// 测试是否安装成功
$ tsc -v
Version 3.4.5

第二步:创建 .ts 文件

// add.ts
function add(x: number, y: number): number {
    const res: number = x + y;
    return res;
}
export default add;

第三步:编译

$ tsc add.ts

clipboard.png

// add.js
function add(x, y) {
    const res = x + y;
    return res;
}
export default add;

文件监听,实时编译

适用于边开发边看结果的情况:

$ tsc -w add.ts
Starting compilation in watch mode...

Watching for file changes.

3、TypeScript 的基本类型

// 布尔值
let isDone: boolean = false;
// 数值
let age: number = 12;
// 字符串
let name: string = 'Jay';
// 数组
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
// 对象
let mokey: object = {name: 'wu kong'};
// 空值,表示没有返回值
function print(): void {}
// 任意值
let goods: any = {};
let goods: any = 2019;
// 未指定类型,视为任意值
let goods;
// 类型推断
let name = 'Jay';
name = 123; // 报错
// 联合类型
let name: string | number;
name = 'Jay';
name = 123;

4、类和接口

类是对属性和方法的封装

类的示例:

// 跟 JavaScript 的类相似,多了访问控制等关键词
class Monkey {
    public height: number;
    private age: number = 12;
    public static mkName: string = 'kinkong';
    private static action: string = 'jump';
    
    constructor(height: number) {
        this.height = height;
    }
    public getAge(): number {
        return this.age;
    }
}

const monkey = new Monkey(120);
monkey.getAge();

const height = monkey.height;
const age = monkey.age; // 报错
const mkName = Monkey.mkName;
const action = Monkey.action; // 报错
类的继承
class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
protected、private、readonly 和 super
  • protected 和 private 都不能从外部访问
  • protected 可以被继承,private 不可以
class Person {
    protected name: string;
    private age: number;
    constructor(name: string, age: number) { 
        this.name = name; 
        this.age = age;
    }
}

class Employee extends Person {
    private readonly initial: string = 'abc';
    private readonly department: string;
    constructor(name: string, department: string) {
        super(name, 1);
        this.department = department;
    }
    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
    public getAge() {
        return this.age;
    }
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // error
let passcode = "secret passcode";

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}
类可以用于接口继承
class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};
接口

接口是用来定义规范的,常常用作类型说明。
接口的示例:

// 定义规范,限定类型
interface Point {
    x: number;
    y: number;
    // 可选属性
    z?: number;
}
let pt: Ponit = {x: 1, y: 1, z: 1};

function setPoint(point: Point): void {}
setPoint({x: 1, y: 1});

// 实现接口
class point implements Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    public getPoint(): object {
        return {
            x: this.x,
            y: this.y,
        }
    }
}

const p = new point(3, 4);
p.getPoint();
接口继承接口
interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}
初始化参数规范
interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

5、type、 namespace 和泛型

type 定义新类型

当 TypeScript 提供的类型不够用时,可以用来自定义类型,供自己使用。

// type 定义新类型,相当于类型别名
type myType = string | number | boolean
// 使用:
const foo: myType = 'foo'

// type 定义函数类型
type hello = (msg: string) => void

// type 对象类型
type WebSite = {
    url: string;
    title: number;
}

namespace 命名空间

命名空间主要有两个方面的用途:

  1. 组织代码。组织一些具有内在联系的特性和对象,能够使代码更加清晰。
  2. 避免名称冲突。
namespace Validator {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }

    const lettersRegexp = /^[A-Za-z]+$/;
    const numberRegexp = /^[0-9]+$/;

// 当声明一个命名空间的时候,所有实体部分默认是私有的,可以使用 export 关键字导出公共部分。
    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }

    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}
new Validator.LettersOnlyValidator();

interface MyClassMethodOptions {
    name?: string;
    age?: number;
}

namespace MyClass {
  export interface MyClassMethodOptions {
    width?: number;
    height?: number;
  }
}
使用外部定义的 namespace
/// <reference path="validate.ts" />
const v = new Validator.LettersOnlyValidator();

泛型

泛型允许在强类型程序设计语言中编写代码时,使用一些以后才指定的类型,在实际调用时才去指定泛型的类型。

举例:

// 模拟服务,提供不同的数据。这里模拟了一个字符串和一个数值
const service = {
    getStringValue: function() {
        return "a string value";
    },
    getNumberValue: function() {
        return 20;
    }
};

// 处理数据的中间件
function middleware(value: string): string {
    return value;
}
// 没问题
middleware(service.getStringValue());
// 报错:参数类型不对
middleware(service.getNumberValue());
// 设成 any 行不行?
function middleware(value: any): any
// 多写几个 middleware 行不行?
function middleware1(value: string): string { ... }
function middleware2(value: number): number { ... }

// 改为泛型
function middleware<T>(value: T): T {
    return value;
}
// 使用
middleware<string>(service.getStringValue());
middleware<number>(service.getNumberValue());

更多内容查看文章:泛型

6、xxx.d.ts声明文件

如果把 add.js 发布成一个 npm 包,别人在使用的时候 IDE 的提示往往都不太友好:

clipboard.png

那么如何能让 IDE 给 add 方法加上类型提示和参数提示呢?

TypeScript 提供了 xxx.d.ts 文件来给 IDE 使用。

xxx.d.ts 叫做声明文件,一般用于类型提示和模块补充,可以自动生成,也可以手动编写,一般情况下是不用我们手动编写的。

// 加上 -d 参数,就会生成 add.d.ts 文件
$ tsc -d add.ts

clipboard.png

// add.d.ts
declare function add(x: number, y: number): number;
export default add;

declare 关键字
用于声明你需要的变量、函数、类等,声明以后 IDE 就可以根据它来进行类型提示。

如果有 add.d.ts 文件,那么它就承担了类型提示的任务:

clipboard.png

同时也相当于 add 方法的接口文档:

clipboard.png

手动创建声明文件

有以下两个场景会去手动创建这个文件:

  1. 给未使用 TypeScript 的 js 模块编写声明文件,可以当做交接文档用;使用别人没有写声明文件的 js 库。
  2. 模块补充。
// 定义模块补充的语法
declare module 'filePath' {
   
}

module-augmentation

使用场景:vue-shim.d.ts 文件,为了让 TypeScript 识别 .vue 文件。

// 文件内容
declare module "*.vue" {
    import Vue from "vue";
    export default Vue;
}

增强类型以配合插件使用

7、配置文件

冗长的参数

指定输出目录:

$ tsc -d --strict -m ESNext --outDir lib index.ts

tsc 的所有参数可以通过执行:tsc -h来查看。

使用 tsconfig.json 配置文件

避免书写冗长的命令,丰富的配置项。

生成 tsconfig.json 文件:

$ tsc --init

常用配置项解读:

{
  "compilerOptions": {
    "target": "ES5", /* target用于指定编译之后的版本目标 version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
    "module": "ESNext", /* 用来指定要使用的模块标准: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "declaration": true,  /* 生成对应的 '.d.ts' 声明文件 */
    "outDir": "./lib", /* 指定编译输出目录 */
    "esModuleInterop": true, /* 通过为导入内容创建命名空间,实现CommonJS和ES模块之间的互操作性 */
    "experimentalDecorators": true, /* 启用 ES7 装饰器语法 */
  }
}

查看更多配置项

配置后,执行以下命令,就会读取配置文件:

$ tsc add.ts

8、使用 TypeScript 开发 web 应用

上述的方法适用于开发命令行工具,比如 @xiyun/cli,开发 web 应用还是不太方便。

如果想要方便地开发 web 应用,比如:开启前端服务、模块热加载等,就需要配合构建工具使用。

配合 webpack 开发 web 应用

点击查看配合 webpack 开发 web 应用源码

配合 parcel,零配置开发 web 应用

parcel:极速零配置 Web 应用打包工具。

联想:可以看成是已经配置好了的 webpack。

全局安装 parcel 工具:

$ yarn global add parcel-bundler

运行:

$ parcel index.html

它就会帮你生成 package.json,自动安装 typeScript,启动好开发服务,并支持热加载。

9、在 Vue 和 React 项目中使用

让 Vue 支持 TypeScript

最方便快捷:vue create my-app,选择 TypeScript。

如果要改造现有项目:

  1. 增加 dev 依赖包:
"devDependencies": {
    "@vue/cli-plugin-typescript": "^3.8.0",
    "typescript": "^3.4.3",
  }
  1. 增加 tsconfig.json 配置
  2. 更改文件后缀:main.js --> main.ts
  3. 组件中指定 lang:
<script lang="ts">
</script>
  1. 在 src 目录下加上:vue-shim.d.ts 文件,为了让 TypeScript 识别 .vue 文件。
// 文件内容
declare module "*.vue" {
    import Vue from "vue";
    export default Vue;
}

Vue 支持 TypeScript 的配置参考

另外一种语法

如果想要用这种语法来写 Vue 组件:

import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;
}

那么需要加上这两个依赖:

"dependencies": {
    // 官网出的ts支持包
    "vue-class-component": "^7.0.2",
    // 对ts支持包更好封装的装饰器
    "vue-property-decorator": "^8.1.0"
  },

Vue 官方对 TypeScript 的说明

创建支持 TypeScript 的 React 项目

创建应用时加上--typescript参数即可:

$ create-react-app my-app --typescript

参考资料

TypeScript 官方文档
TypeScript Declare Keyword
Vue 对 模块补充的说明


kybetter
483 声望10 粉丝

学无止境