进击TypeScript(入门及实战应用)

前言

2020年,越来越多的前端开源项目已经或者正在使用TypeScript重构(ant design、vs code 、vue3等)。正在从事前端开发的我们,已经不能忽视TypeScript整个生态链对前端开发的影响。

首先我们看一下下面这张图片: compile time error vs runtime error。

由图片可知,编译时报错我们还能有所补救,但是当bug一旦到了线上,造成的就是系统页面的崩溃,代价可就大了。


下面这张图是stackoverflow网站2019年做的一个开发者调查报告。如图所示,TypeScript在最受开发者喜爱的语言排行榜上已经与python并列第二。

此外,从下面的图片可以看出在前端项目中大多bug都是类型导致的。


一个叫做 “To Type or Not to Type: Quantifying Detectable Bugs in JavaScript” 的研究表明,在选择 TypeScript 的 github 仓库中,有 15% 的 bug 得到了避免。

另一个统计调查表明:2018年,在npm包的开发者中,有62%的人在使用TS。

由此可见,TypeScript已经势不可挡。


JavaScript存在的问题

那是什么导致TS的出现呢?从根本原因来讲,JavaScript是一门动态弱类型语言,对变量的类型很宽容,容易导致一些低级错误。

比如我们常犯的手误错误(一个单词多输入了一个字符):下面的代码如果用TypeScript,IDE会自动提示标红,这样就避免把bug带到线上环境。

const regions = [
    {
        label: '四川省',
        value: 1
    },
    {
        label: '泸州市',
        value: 20
    },
    {
        label: '龙马潭区',
        value: 3310
    }
];
const region = regions.map((ele) => ele.values); 
// 属性“values”在类型“{ label: string; value: number; }”上不存在。你是否指的是“value”?ts(2551)


编程语言弱类型与强类型的区别

  • 强类型: 强制数据类型定义语言,类型安全的语言(若定义了一个整型变量a,若不进行显示转换,不能将a当作字符串类型处理)
  • 弱类型:则没有约束,相对比较灵活( 一个变量可以赋不同数据类型的值)

编程语言动态语言与静态语言的区别

  • 静态语言:在编译阶段确定所有变量的类型
  • 动态语言: 在执行阶段确定所有变量的类型

TypeScript是静态弱类型语言,不是强类型是为了兼容JS,所以TypeScript不会限制JavaScript原有的隐式类型转换,但真正的强类型语言是Java、C#是不会容忍隐式转换的。


什么是TypeScript?

来自官网的定义:TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。TypeScript 编译工具可以运行在任何服务器和任何系统上。TypeScript 是开源的。

TypeScript更像是一门工具,而不是一门独立的语言。


TypeScript带来了哪些好处?

  • 类型安全:会在编译代码时,进行严格的类型检查。可以在编译阶段就发现大部分错误,这总比上线的时候出错好。
  • 下一代JS特性
  • 完善的工具链
  • 提高生产力
  • 增加了代码的可读性和可维护性:代码及文档
  • 搭配VS code 等编辑器使用:代码提示、自动补全、自动import等功能
  • 强大的社区支持

如何判断是否需要TypeScript?

  • 项目的规模是怎样的
  • 项目是否存在多人合作
  • 是否会有新人员加入
  • 项目是否需要长期维护


快速介绍:静态类型基础

TypeScript基础数据类型

ES6有如下数据类型:Boolean、Number、String、Array、Function、Object、Symbol、undefined和null。

TypeScript在ES6的基础上增加了以下数据类型:

  • void: 表示没有任何返回值的类型
  • never: 表示永远不会有返回值的类型(应用:函数抛出异常、死循环函数)
  • 元组:元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。
  • 枚举
  • 高级类型: 条件类型/映射类型/索引类型等
  • any
  • unknown

关于undefined和null说明

我们可以给变量声明undefined和null,但是如果声明了undefined,就不可以再给变量赋值其它类型。

undefined和null是其它类型的子类型,在tsconfig.js中把strictNullChecks设置为false就不会报错了。

或者使用联合类型。在num的类型注解中加上undefined和null。

一个概念:集合理论

在TypeScript中,“类型”可以理解为值的集合。类型 string 即字符串的集合。

any和unknown为一切类型的超级类型(即顶端类型),任何值都可以注解为any或者unknown,

never 是一切其它非空类型的子集合。所以never为底端类型。

TypeScript通过操作符 union(|)和 intersection(&)来识别顶端类型和底端类型,比如以下:

T | never => T // never与T取并集为T,这可以看出never就是一个空集合,任何值都不能注解为never。
T & unknown => T // unknown与T取交集为T,所以其它类型为unknown类型的子集合。

never

// never
let error = () => {
    throw new Error('error')
}
let endless = () => {
    while(true) {}
}

另一个应用是在条件类型中有运用到never: NonNullable 类型就是例子,它是将 null和 undefined 类型从 T 中排除。其定义如下:

type NonNullable<T> = T extends null | undefined ? never : T
type T0 = NonNullable<string | number | undefined>;  // string | number
type T1 = NonNullable<string[] | null | undefined>;  // string[]

unknown和any的区别

unknown更加严格,在对unknown类型执行大多数操作前(实例化、getter、函数执行)等,都要线缩小其类型范围。

let value: any;

value.foo.bar;  // OK
value.trim();   // OK
value();        // OK
new value();    // OK
value[0][1];    // OK


let value2: unknown;

value2.foo.bar;  // Error
value2.trim();   // Error
value2();        // Error
new value2();    // Error
value2[0][1];    // Error

unknown将类型缩小为更具体的类型, 从而实现TS的类型保护功能。

/**
 * A custom type guard function that determines whether
 * `value` is an array that only contains numbers.
 */
function isNumberArray(value: unknown): value is number[] {
    return Array.isArray(value) && value.every((element) => typeof element === 'number');
}

const unknownValue: unknown[] = [ 15, 23, 8, 4, 42, 16 ];
const unknownValue2: unknown[] = [ '1', 23, 8, 4, 42, 16 ];

if (isNumberArray(unknownValue)) {
    // Within this branch, `unknownValue` has type `number[]`,
    // so we can spread the numbers as arguments to `Math.max`
    const max = Math.max(...unknownValue);
    console.log(max);
}

类型注解和类型推断

类型注解

相当于强类型语言中的类型声明

语法: (变量/函数): type

// 原始类型
const user: string = "Echo Zhou"
const haha: boolean = false

// 函数
function warnUser(): void {
    alert("This is my warning message");
}

// 数组
let arr1: number[] = [1, 2, 3]

// 元组
let tuple: [number, string] = [0, '1']

类型推断

什么是类型推断:

不需要指定变量的类型(函数的返回值类型),TypeScript可以根据某些规则自动地为其推断出一个类型。

类型断言

类型断言是指可以手动指定一个类型,允许变量从一个类型更改为另一个类型

交叉类型与联合类型

交叉类型就是把多个类型合并成一个类型。

联合类型: 声明的类型并不确定,可以为多个类型中的一个。


TypeScript核心

鸭子辨型法

什么是鸭子辨型法呢?如果一个对象走起路来像鸭子,嘎嘎叫起来像鸭子,那么我们就说这个对象是鸭子。

TypeScript其实就是对值所具有的结构进行类型检查,所以TS也可以被称作鸭子辨型法。

接口

为约束对象、函数、类的结构定义的一种契约(形状)

interface IWarningModProps {
    form: FormProps['form'];
    visible: boolean;
    handleModalVisible: (flag: boolean) => void;
    closeModal: () => void;
    threshold: FetchCreThresholdSuccData['info']
    customerId: number;
}

成员修饰及抽象类、抽象方法

private: 只能被类的本身调用,而不能被类的实例和子类调用,构造函数加private,表明这个类不能被实例化,也不能被继承。

protected: 只能在类和子类中调用,实例中不能调用

static(类的静态成员修饰符): 只能通过类名调用

抽象类与抽象方法:用于实现多态,提取共性(父类中定义抽象方法,在子类中可以有不同的实现)。

abstract class A {
    public A1: string = 'TS';
    private A2: number = 123;
    protected A3: string = 'GOOD';
    static A4: number = 123;

    public FA1() {
        console.log('123');
    }
    private FA2() {}
    protected FA3() {}
    static FA4() { // 抽象方法必须被子类实现
        console.log('Dog sleep');
    }
    abstract FA5(): void;
}

class B extends A {
    public B1: string = '123';
    private B2: number = 123;
    protected B3: string = '1234';
    static B4: number = 1234;

    F5() {}
    private FB5() {}
    protected FB6() {}
    static FB7() {
        console.log('Dog sleep');
    }
    public FA5() {}
}
//实例化 B
let b = new B();

类与接口

  • 接口(Interface)主要表示抽象的行为,不能初始化属性和方法,当类实现接口时就可以具体化行为了。
  • 类可以implements实现接口的时候,必须实现接口里的所有属性和方法。
  • 一个接口可以继承多个接口,直接使用extend关键字,每个接口用逗号隔开即可。
  • 一个类只能继承另一个类,若要实现一个类别继承多个类需要使用Mixins。
// 通过接口提取不同类之间的共性,类implements接口提高了面向对象的灵活性
interface Alarm {
    alert(): void;
}

interface Light {
    lightOn(): void;
    lightOff(): void;
}

class Car implements Alarm, Light {
    alert() {
        console.log('Car alert');
    }
    lightOn() {
        console.log('Car light on');
    }
    lightOff() {
        console.log('Car light off');
    }
}

interface和type关键字

  • 首先type类型别名比起interface,除了定义对象类型以外,还可以定义交叉、联合、原始类型,相比于interface更加广泛。
  • 当我们需要合并两个类型,虽然interface的extends也可以实现声明合并,但是type的交叉类型更加易读。此时建议使用type。
  • 但是interface可以实现接口的extends 和 implements。interface可以实现接口的合并声明。type可以使用交叉类型代替实现。

一般情况下,当我们需要开发一个公共库的时候,最好使用interface,interface的继承与实现特性有利于使用者扩展。

interface MilkshakeProps {
  name: string;
  price: number;
}

interface MilkshakeMethods {
  getIngredients(): string[];
}

// 接口的声明合并
interface Milkshake extends MilkshakeProps, MilkshakeMethods {}
// 相当于type的交叉类型
type Milkshake = MilkshakeProps & MilkshakeMethods;

泛型

泛型无处不在,不论是第三方库的声明文件,还是我们自己的实践项目中,泛型能够保持代码的可读性、简洁性,又不失程序的灵活性。


(来自一篇文章读懂Typescript泛型及应用:juejin.im/post/5ee00f…

理解泛型有两个要点:

  • 不需要预先确定的数据类型,具体的类型在使用的时候才能确定。
  • 泛型就像函数中的参数一样(我们可以理解为类型参数),在定义一个函数、type、interface、class 时,在名称后面加上<>表示其值也可以作为参数传递。

实践一:React类组件

在React声明文件中,对它所有api都进行了重新定义。

比如Component被定义成了一个泛型类,这个泛型类接收三个参数

  • P:默认为空对象,为属性类型
  • S:默认为空对象,为状态类型
  • SS:不用管。
interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }
interface DirProps {
  // ...此处省略
}
class Dir extends Component<DirProps, DirState> {
}

实践二:泛型给使用 API 提供了便利

在项目中,我们通常会去扩展或者二次封装axios,比如一个实际的项目中,有一个 request.js 文件,我们定义了一个Axios工具类,提供get、post、put等公共方法,然后进行实例化导出以供使用。

请看以下例子:

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { RemoveStorage } from '@/utils/utils';
import { GetStorage } from '@/utils/utils';
import { message } from 'antd';


const axiosInstance = axios.create(axiosConfig)


export interface IHttpClient {
    getStoreConfig: () => {
        baseURL: string;
        headers: {
            webToken: string;
        };
    };
    get: <T>(url: string, config?: AxiosRequestConfig) => Promise<T>;
    post: <T>(url: string, data?: any, config?: AxiosRequestConfig) => Promise<T>;
}
...// 此处省略 ,例如添加通用配置、拦截器等。
const _http = axiosInstance;
class Axios implements IHttpClient {
    public getStoreConfig() {
        return {
            baseURL: '/ops-crm/wapi',
            headers: { webToken: GetStorage('webToken') }
        };
    }

    public async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
        const configInfo = config ? config : this.getStoreConfig();
        const response: AxiosResponse = await _http.get(url, configInfo);
        return response;
    }

    public async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
        const configInfo = config ? config : this.getStoreConfig();
        const response: AxiosResponse = await _http.post(url, data, configInfo);
        return response;
    }
}

export const fetch: IHttpClient = new Axios();

你肯定有疑问🤔️了?

get(url: string, config?: AxiosRequestConfig ) => Promise<T> ?

如何理解Promise<T>中的泛型T?

  • 你可以理解为promise变成成功态之后resolve的值,即传递给resolve的参数类型。

我们看下面这个例子:

当你没有传入泛型的时候 ,ts会推导出Promise(变成成功态以后)的返回值类型是unknown。

我们只需要作以下修改,TypeScript就可以推导出Promise返回值类型为number。

所以我们在写Api的时候就可以通过泛型T进行传递类型,然后TypeScript也可以推导出接口的返回值类型。

以下是TS自带声明文件中Promise构造函数的TS类型。

/// <reference no-default-lib="true"/>

interface PromiseConstructor {
    /**
     * A reference to the prototype.
     */
    readonly prototype: Promise<any>;

    /**
     * Creates a new Promise.
     * @param executor A callback used to initialize the promise. This callback is passed two arguments:
     * a resolve callback used to resolve the promise with a value or the result of another promise,
     * and a reject callback used to reject the promise with a provided reason or error.
     */
    new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
    ...

    /**
     * Creates a new rejected promise for the provided reason.
     * @param reason The reason the promise was rejected.
     * @returns A new rejected Promise.
     */
    reject<T = never>(reason?: any): Promise<T>;

    /**
     * Creates a new resolved promise for the provided value.
     * @param value A promise.
     * @returns A promise whose internal state matches the provided promise.
     */
    resolve<T>(value: T | PromiseLike<T>): Promise<T>;

    /**
     * Creates a new resolved promise .
     * @returns A resolved promise.
     */
    resolve(): Promise<void>;
}

declare var Promise: PromiseConstructor;

高级特性

索引类型

typeof : 获取 JS 值的类型

typeof 操作符可以用来获取一个变量声明或对象的类型。

keyof T:获取类型的键

索引查询操作符, 可以用于获取某种类型的所有键,其返回类型是联合类型。

所以这两个操作符经常联合一起使用:

const defaultColConfig = {
  xs: 24,
  sm: 24,
  md: 12,
  lg: 12,
  xl: 8,
  xxl: 6,
};
type colConfig = keyof typeof defaultColConfig 
// type colConfig = "xs" | "sm" | "md" | "lg" | "xl" | "xxl"

T[K]:获取类型的值

应用场景: 从一个对象中选取某些属性的值

interface Iobj1 {
    a: number;
}
type typea = Iobj1['a']; // string

let obj1 = {
    a: 1,
    b: 2,
    c: 3
};
function getValues<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
    return keys.map((key) => obj[key]);
}
const a1 = getValues(obj1, [ 'a', 'b' ]); // [1,2]

其它相关应用

比如在TypeScript内置类型Omit、Record等都用到了keyof。

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Record<K extends keyof any, T> = { [P in K]: T };

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>; 
/* type ThreeStringProps = {
    prop1: string;
    prop2: string;
    prop3: string;
} */

映射类型

定义:从旧类型创建出新类型

// Partial: 将一个map所有属性变为可选的
type Partial<T> = { [P in keyof T]?: T[P] } 
interface Obj {
    a: string;
    b: number;
}
type PartialObj = Partial<Obj>

// Pick: 提取map中想要的属性
type PickObj = Pick<Obj, 'a' | 'b'> 
interface Customer {
    id: number;
    name: string;
    address: string;
    region: string;
    parent: number;
}
type KeyProps = Pick<Customer, 'id' | 'name'>;

// Record: 构造具有类型K为type 的属性的类型T, 可用于将一个类型的属性映射到另一个类型。
type Record<K extends keyof any, T> = { [P in K]: T }; 

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>; 
/* type ThreeStringProps = {
    prop1: string;
    prop2: string;
    prop3: string;
} */

type RecordObj = Record<'x' | 'y', Obj>

条件类型: infer及extends关键字

extends关键字

在条件语句中,extends不是继承的意思,比如这个条件语句T extends U ? X : Y中的extends应当这样理解:如果类型T可以赋值给类型U,那么结果类型就是X,否则为Y。

infer关键字

infer :表示在 extends 条件语句中, TypeScript进行类型推断,并将推断结果存储在infer关键字后面的变量中。

type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;

  • 上面这个条件语句中,P 表示待推断的函数返回值的类型。

实际应用场景:

type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;

function transCus () {
  return {
    name: 'echo',
    region: ['四川省', '泸州市', '龙马潭区'],
    address: 'xxx',
    metaType: 3,
    type: 3
  }
}
type Customer = ReturnType<typeof transCus> 
/*
type Customer = {
    name: string;
    region: string[];
    address: string;
    metaType: number;
    type: number;
}
*/

条件类型还有其它很多关键字如下,具体就不一一解释了。

  • Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
  • Extract<T, U> -- 提取T中可以赋值给U的类型。
  • NonNullable<T> -- 从T中剔除null和undefined。
  • InstanceType<T> -- 获取构造函数类型的实例类型。

工程相关

声明文件

一般以d.ts为后缀的,我们称之为声明文件。

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

当我们在使用非ts编写的类库(jquery)的时候,必须为这个类库编写一个声明文件,对外暴露它的api。

有时候,一些类库的声明文件是编写在源码中(比如antd),有时候是单独提供的,需要额外的安装(比如jquery)。

大多数类库的声明文件 ,社区已为我们编写好了。我们只需要安装即可。

npm i @types/jquery -D

贡献社区:definitelytyped.org/guides/cont…

总结: 所以当库本身没有自带声明文件的时候,我们需要从DefinitelTyped下载安装,如果连DefinitelTyped都没有,比如我们公司自己的第三方组件或库,那么就需要我们declare module 一下。

当我们需要给其它类库自定义方法

// 模块插件
import m from 'moment';
declare module 'moment' {
    export function myFunction(): void;
}
m.myFunction = () => {}

了解更多:ts.xcatliu.com/basics/decl…

配置文件tsconfig.json

files: 表示编译器需要编译的单个文件的列表。

include: [] 支持通配符,编译器需要编译的文件或者目录。

编译选项:

  • target: 我们要编译成的目标语言是什么版本

'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'

  • module: 要把我们的代码编译成什么模块系统

'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.

{
   "files": [
     "src/a.ts"
   ],
   "include": [
    "src/*" // 只编译src一级目录下的文件
   ],
  "compilerOptions": {
      // "incremental": true,                // 增量编译
      // "tsBuildInfoFile": "./buildFile",   // 增量编译文件的存储位置
      // "diagnostics": true,                // 打印诊断信息

      // "target": "es5",           // 目标语言的版本
      // "module": "commonjs",      // 生成代码的模块标准
      // "outFile": "./app.js",     // 将多个相互依赖的文件生成一个文件,可以用在 AMD 模块中

      // "lib": [],                 // TS 需要引用的库,即声明文件,es5 默认 "dom", "es5", "scripthost"

      // "allowJs": true,           // 允许编译 JS 文件(js、jsx)
      // "checkJs": true,           // 允许在 JS 文件中报错,通常与 allowJS 一起使用
      // "outDir": "./out",         // 指定输出目录
      // "rootDir": "./",           // 指定输入文件目录(用于输出)

      // "declaration": true,         // 会为我们自动生成声明文件
      // "declarationDir": "./d",     // 声明文件的路径
      // "emitDeclarationOnly": true, // 只生成声明文件
      // "sourceMap": true,           // 生成目标文件的 sourceMap
      // "inlineSourceMap": true,     // 生成目标文件的 inline sourceMap
      // "declarationMap": true,      // 生成声明文件的 sourceMap
      // "typeRoots": [],             // 声明文件目录,默认 node_modules/@types
      // "types": [],                 // 声明文件包

      // "removeComments": true,    // 删除注释

      // "noEmit": true,            // 不输出文件
      // "noEmitOnError": true,     // 发生错误时不输出文件

      // "noEmitHelpers": true,     // 不生成 helper 函数,需额外安装 ts-helpers
      // "importHelpers": true,     // 通过 tslib 引入 helper 函数,文件必须是模块

      // "downlevelIteration": true,    // 降级遍历器的实现(es3/5)

      // "strict": true,                        // 开启所有严格的类型检查
      // "alwaysStrict": false,                 // 在代码中注入 "use strict";
      // "noImplicitAny": false,                // 不允许隐式的 any 类型
      // "strictNullChecks": false,             // 不允许把 null、undefined 赋值给其他类型变量
      // "strictFunctionTypes": false           // 不允许函数参数双向协变
      // "strictPropertyInitialization": false, // 类的实例属性必须初始化
      // "strictBindCallApply": false,          // 严格的 bind/call/apply 检查
      // "noImplicitThis": false,               // 不允许 this 有隐式的 any 类型

      // "noUnusedLocals": true,                // 检查只声明,未使用的局部变量
      // "noUnusedParameters": true,            // 检查未使用的函数参数
      // "noFallthroughCasesInSwitch": true,    // 防止 switch 语句贯穿
      // "noImplicitReturns": true,             // 每个分支都要有返回值

      // "esModuleInterop": true,               // 允许 export = 导出,由import from 导入
      // "allowUmdGlobalAccess": true,          // 允许在模块中访问 UMD 全局变量
      // "moduleResolution": "node",            // 模块解析策略
      // "baseUrl": "./",                       // 解析非相对模块的基地址
      // "paths": {                             // 路径映射,相对于 baseUrl
      //   "jquery": ["node_modules/jquery/dist/jquery.slim.min.js"]
      // },
      // "rootDirs": ["src", "out"],            // 将多个目录放在一个虚拟目录下,用于运行时

      // "listEmittedFiles": true,        // 打印输出的文件
      // "listFiles": true,               // 打印编译的文件(包括引用的声明文件)
  }
}

编译工具

ts-loader:

  • 把typescript编译成javascript
  • 配置项transpileOnly:只做语言转换,不做类型检查。
  • 插件:fork-ts-checker-webapck-plugin独立的类型检查进程。

awesome-typescript-loader

与ts-loader的主要区别:

  • 更适合与babel集成,使用babel转义和缓存。
  • 不需要安装额外的插件,就可以把类型检查放在独立进程中进行。

为什么使用了TypeScript,还需要使用babel?

tsc和babel都具有语言转换功能,区别在于tsc具有代码检查而babel没有,但是babel有丰富的插件。

babel: 只做语言转换的插件

  • @babel/preset-typescript
  • @babel/proposal-class-properties
  • @babel/proposal-object-rest-spread

大家可以看这篇文章:

juejin.im/post/5c822e…



原文链接:juejin.im

上一篇:一文了解React中的受控组件与非受控组件
下一篇:带你认识和理解Promise

相关推荐

官方社区

扫码加入 JavaScript 社区