【THE LAST TIME】Typescript 进阶 之 重难点梳理

THE LAST TIME

The last time, I have learnedhtml

【THE LAST TIME】 一直是我想写的一个系列,旨在厚积薄发,重温前端。前端

也是给本身的查缺补漏和技术分享。node

笔者文章集合详见git

前言

JavaScript 毋庸置疑是一门很是好的语言,可是其也有不少的弊端,其中不乏是做者设计之处留下的一些 “bug”。固然,瑕不掩瑜~github

话说回来,JavaScript 毕竟是一门弱类型语言,与强类型语言相比,其最大的编程陋习就是可能会形成咱们类型思惟的缺失(高级词汇,我从极客时间学到的)。而思惟方式决定了编程习惯,编程习惯奠基了工程质量,工程质量划定了能力边界,而学习 Typescript,最重要的就是咱们类型思惟的重塑。web

那么其实,Typescript 在我我的理解,并不能算是一个编程语言,它只是 JavaScript 的一层壳。固然,咱们彻底能够将它做为一门语言去学习。网上有不少推荐 or 不推荐 Typescript 之类的文章这里咱们不作任何讨论,学与不学,用或不用,利与弊。各自拿捏~typescript

再说说 typescript(下文均用 ts 简称),其实对于 ts 相比你们已经不陌生了。更多关于 ts 入门文章和文档也是已经烂大街了。此文不去翻译或者搬运各类 api或者教程章节。只是总结罗列和解惑,笔者在学习 ts 过程当中曾疑惑的地方。道不到的地方,欢迎你们评论区积极讨论。编程

其实 Ts 的入门很是的简单:.js to .ts; over!segmentfault

可是为何我都会写 ts 了,却看不懂别人的代码呢? 这!就是入门与进阶之隔。也是本文的目的所在。api

首先推荐下 ts 的编译环境:typescriptlang.org

再推荐笔者收藏的几个网站:

下面,逐个难点梳理,逐个击破。

可索引类型

关于ts 的类型应该不用过多介绍了,多用多记 便可。介绍下关于 ts 的可索引类型。准确的说,这应该属于接口的一类范畴。说到接口(interface),咱们都知道 ts 的核心原则之一就是对值所具备的结构进行类型检查。 它有时被称之为“鸭式辩型法”或“结构性子类型”。而接口就是其中的契约。可索引类型也是接口的一种表现形式,很是实用!

interface StringArray {
[index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];
复制代码

上面例子里,咱们定义了StringArray接口,它具备索引签名。 这个索引签名表示了当用number去索引StringArray时会获得string类型的返回值。 Typescript支持两种索引签名:字符串和数字。 能够同时使用两种类型的索引,可是数字索引的返回值必须是字符串索引返回值类型的子类型。

这是由于当使用number来索引时,JavaScript会将它转换成string而后再去索引对象。 也就是说用100(一个number)去索引等同于使用"100"(一个string)去索引,所以二者须要保持一致。

class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}

// 错误:使用数值型的字符串索引,有时会获得彻底不一样的Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
复制代码

下面的例子里,name的类型与字符串索引类型不匹配,因此类型检查器给出一个错误提示:

interface NumberDictionary {
[index: string]: number;
length: number; // 能够,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}
复制代码

固然,咱们也能够将索引签名设置为只读,这样就能够防止给索引赋值

interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
复制代码

interface 和 type 关键字

stackoverflow 上的一个高赞回答仍是很是赞的。typescript-interfaces-vs-types

interfacetype 两个关键字的含义和功能都很是的接近。这里咱们罗列下这两个主要的区别:

interface

  • 同名的 interface 自动聚合,也能够跟同名的 class 自动聚合
  • 只能表示 objectclassfunction 类型

type:

  • 不只仅可以表示 objectclassfunction
  • 不能重名(天然不存在同名聚合了),扩展已有的 type 须要建立新 type
  • 支持复杂的类型操做

举例说明下上面罗列的几点:

Objects/Functions

均可以用来表示 Object 或者 Function ,只是语法上有些不一样而已

interface Point{
x:number;
y:number;
}

interface SetPoint{
(x:number,y:number):void;
}
复制代码
type Point = {
x:number;
y:number;
}

type SetPoint = (x:number,y:number) => void;
复制代码

其余数据类型

interface 不一样,type 还能够用来标书其余的类型,好比基本数据类型、元素、并集等

type Name = string;

type PartialPointX = {x:number;};
type PartialPointY = {y:number;};

type PartialPoint = PartialPointX | PartialPointY;

type Data = [number,string,boolean];
复制代码

Extend

均可以被继承,可是语法上会有些不一样。另外须要注意的是,interface 和 type 彼此并不互斥

interface extends interface

interface PartialPointX {x:number;};
interface Point extends PartialPointX {y:number;};
复制代码

type extends type

type PartialPointX = {x:number;};
type Point = PartialPointX & {y:number;};
复制代码

interface extends type

type PartialPointX = {x:number;};
interface Point extends PartialPointX {y:number;};
复制代码

type extends interface

interface ParticalPointX = {x:number;};

type Point = ParticalPointX & {y:number};
复制代码

implements

一个类,能够以彻底相同的形式去实现interface 或者 type。可是,类和接口都被视为静态蓝图(static blueprints),所以,他们不能实现/继承 联合类型的 type

interface Point {
x: number;
y: number;
}

class SomePoint implements Point {
x: 1;
y: 2;
}

type Point2 = {
x: number;
y: number;
};

class SomePoint2 implements Point2 {
x: 1;
y: 2;
}

type PartialPoint = { x: number; } | { y: number; };

// FIXME: can not implement a union type
class SomePartialPoint implements PartialPoint {
x: 1;
y: 2;
}
复制代码

声明合并

type 不一样,interface 能够被重复定义,而且会被自动聚合

interface Point {x:number;};
interface Point {y:number;};

const point:Pint = {x:1,y:2};
复制代码

only interface can

在实际开发中,有的时候也会遇到 interface 可以表达,可是 type 作不到的状况:给函数挂载属性

interface FuncWithAttachment {
(param: string): boolean;
someProperty: number;
}

const testFunc: FuncWithAttachment = function(param: string) {
return param.indexOf("Neal") > -1;
};
const result = testFunc("Nealyang"); // 有类型提醒
testFunc.someProperty = 4;
复制代码

& 和 | 操做符

这里咱们须要区分,|& 并不是位运算符。咱们能够理解为&表示必须同时知足全部的契约。|表示能够只知足一个契约。

interface IA{
a:string;
b:string;
}

type TB{
b:number;
c:number [];
}

type TC = TA | TB;// TC 的 key,包含 ab 或者 bc 便可,固然,包含 bac 也能够
type TD = TA & TB;// TD 的 能够,必须包含 abc
复制代码

交叉类型

交叉类型,咱们能够理解为合并。其实就是将多个类型合并为一个类型

Man & WoMan
复制代码
  • 同时是 Man 和 Woman
  • 同时拥有 Man 和 Woman 这两种类型的成员
interface ObjectConstructor{
assign<T,U>(target:T,source:U):T & U;
}
复制代码

以上是 ts 的源码实现,下面咱们再看一个咱们平常使用中的例子

interface A{
name:string;
age:number;
sayName:(name:string)=>void
}

interface B{
name:string;
gender:string;
sayGender:(gender:string)=>void
}

let a:A&B;

// 这是合法的
a.age
a.sayGender
复制代码

注意:16446

T & never = never 
复制代码

extends

extends 即为扩展、继承。在 ts 中,extends 关键字既能够来扩展已有的类型,也能够对类型进行条件限定。在扩展已有类型时,不能够进行类型冲突的覆盖操做。例如,基类型中键astring,在扩展出的类型中没法将其改成number

type num = {
num:number;
}

interface IStrNum extends num {
str:string;
}

// 与上面等价
type TStrNum = A & {
str:string;
}
复制代码

在 ts 中,咱们还能够经过条件类型进行一些三目操做:T extends U ? X : Y

type IsEqualType<A , B> = A extends B ? (B extends A ? true : false) : false;

type NumberEqualsToString = IsEqualType<number,string>; // false
type NumberEqualsToNumber = IsEqualType<number,number>; // true
复制代码

keyof

keyof 是索引类型操做符。用于获取一个“常量”的类型,这里的“常量”是指任何能够在编译期肯定的东西,例如constfunctionclass等。它是从 实际运行代码 通向 类型系统 的单行道。理论上,任何运行时的符号名想要为类型系统所用,都要加上 typeof

在使用class时,class名表示实例类型,typeof class表示 class自己类型。是的,这个关键字和 js 的 typeof 关键字重名了 。

假设 T 是一个类型,那么keyof T产生的类型就是 T 的属性名称字符串字面量类型构成的联合类型(联合类型比较简单,和交叉类型对立类似,这里就不作介绍了)。

注意!上述的 T 是数据类型,并不是数据自己

interface IQZQD{
cnName:string;
age:number;
author:string;
}
type ant = keyof IQZQD;
复制代码

vscode 上,咱们能够看到 ts 推断出来的 ant

注意,若是 T 是带有字符串索引的类型,那么keyof Tstring或者number类型。

索引签名参数类型必须为 "string" 或 "number"

interface Map<T> {
[key: string]: T;
}

//T[U]是索引访问操做符;U是一个属性名称。
let keys: keyof Map<number>; //string | number
let value: Map<number>['antzone'];//number
复制代码

泛型

泛型多是对于前端同窗来讲理解起来有点困难的知识点了。一般咱们说,泛型就是指定一个表示类型的变量,用它来代替某个实际的类型用于编程,然后再经过实际运行或推导的类型来对其进行替换,以达到一段使用泛型程序能够实际适应不一样类型的目的。说白了,泛型就是不预先肯定的数据类型,具体的类型在使用的时候再肯定的一种类型约束规范

泛型能够应用于 functioninterfacetype 或者 class 中。可是注意,泛型不能应用于类的静态成员

几个简单的例子,先感觉下泛型

function log<T>(value: T): T {
console.log(value);
return value;
}

// 两种调用方式
log<string[]>(['a', ',b', 'c'])
log(['a', ',b', 'c'])
log('Nealyang')
复制代码
  • 泛型类型、泛型接口
type Log = <T>(value: T) => T
let myLog: Log = log

interface Log<T> {
(value: T): T
}
let myLog: Log<number> = log // 泛型约束了整个接口,实现的时候必须指定类型。若是不指定类型,就在定义的以后指定一个默认的类型
myLog(1)
复制代码

咱们也能够把泛型变量理解为函数的参数,只不过是另外一个维度的参数,是表明类型而不是表明值的参数。

class Log<T> { // 泛型不能应用于类的静态成员
run(value: T) {
console.log(value)
return value
}
}

let log1 = new Log<number>() //实例化的时候能够显示的传入泛型的类型
log1.run(1)
let log2 = new Log()
log2.run({ a: 1 }) //也能够不传入类型参数,当不指定的时候,value 的值就能够是任意的值
复制代码

类型约束,需预约义一个接口

interface Length {
length: number
}
function logAdvance<T extends Length>(value: T): T {
console.log(value, value.length);
return value;
}

// 输入的参数无论是什么类型,都必须具备 length 属性
logAdvance([1])
logAdvance('123')
logAdvance({ length: 3 })
复制代码

泛型的好处:

  • 函数和类能够轻松的支持多种类型,加强程序的扩展性
  • 没必要写多条函数重载,冗长的联合类型声明,加强代码的可读性
  • 灵活控制类型之间的约束

泛型,在 ts 内部也都是很是经常使用的,尤为是对于容器类很是经常使用。而对于咱们,仍是要多使用,多思考的,这样才会有更加深入的体会。同时也对塑造咱们类型思惟很是的有帮助。

小试牛刀

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}

interface Person {
name: string;
age: number;
}

let person: Person = {
name: 'Jarid',
age: 35
};
let strings: string[] = pluck(person, ['name', 'name', 'name']); //["Jarid", "Jarid", "Jarid"]
复制代码

所谓的小试牛刀,就是结合上面咱们说的那几个点,分析下pluck方法的意思

  • <T, K extends keyof T>约束了这是一个泛型函数
    • keyof T 就是取 T 中的全部的常量 key(这个例子的调用中),即为: "name" | "age"
    • K extends keyof Person 即为 K 是 "name" or "age"
  • 结合以上泛型解释,再看形参
    • K[] 即为 只能包含 "name" or "age"的数组
  • 再看返回值
    • T[K][] 后面的 []是数组的意思。而 T[K]就是去对象的 T 下的 key: Kvalue

infer

infer 关键字最先出如今 PR 里面,表示在 extends 条件语句中待推断的类型变量

是在 ts2.8 引入的,在条件判断语句中,该关键字用于替换手动获取类型

type PramType<T> = T extends (param : infer p) => any ? p : T;
复制代码

在上面的条件语句中,infer P 表示待推断的函数参数,若是T能赋值给(param : infer p) => any,则结果是(param: infer P) => any类型中的参数 P,不然为T.

interface INealyang{
name:'Nealyang';
age:'25';
}

type Func = (user:INealyang) => void;

type Param = ParamType<Func>; // Param = INealyang
type Test = ParamType<string>; // string
复制代码

工具泛型

所谓的工具泛型,其实就是泛型的一些语法糖的实现。彻底也是能够本身的写的。咱们也能够在lib.d.ts中找到他们的定义

Partial

Partial的做用就是将传入的属性变为可选。

因为 keyof 关键字已经介绍了。其实就是能够用来取得一个对象接口的全部 key 值。在介绍 Partial 以前,咱们再介绍下 in 操做符:

type Keys = "a" | "b"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any }
复制代码

而后再看 Partial 的实现:

type Partial<T> = { [P in keyof T]?: T[P] };
复制代码

翻译一下就是keyof T 拿到 T 全部属性名, 而后 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值,而后配合?:改成可选。

Required

Required 的做用是将传入的属性变为必选项, 源码以下

type Required<T> = { [P in keyof T]-?: T[P] };
复制代码

Readonly

将传入的属性变为只读选项, 源码以下

type Readonly<T> = { readonly [P in keyof T]: T[P] };
复制代码

Record

该类型能够将 K 中全部的属性的值转化为 T 类型,源码实现以下:

/**
* Construct a type with a set of properties K of type T
*/

type Record<K extends keyof any, T> = {
[P in K]: T;
};
复制代码

能够根据 K 中的全部可能值来设置 key,以及 value 的类型,举个例子:

type T11 = Record<'a' | 'b' | 'c', Person>; // -> { a: Person; b: Person; c: Person; }
复制代码

Pick

T 中取出 一系列 K 的属性

/**
* From T, pick a set of properties whose keys are in the union K
*/

type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
复制代码

Exclude

Exclude 将某个类型中属于另外一个的类型移除掉。

/**
* Exclude from T those types that are assignable to U
*/

type Exclude<T, U> = T extends U ? never : T;
复制代码

以上语句的意思就是 若是 T 能赋值给 U 类型的话,那么就会返回 never 类型,不然返回 T,最终结果是将 T 中的某些属于 U 的类型移除掉

举个栗子:

type T00 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>;  // -> 'b' | 'd'
复制代码

能够看到 T'a' | 'b' | 'c' | 'd' ,而后 U'a' | 'c' | 'f' ,返回的新类型就能够将 U 中的类型给移除掉,也就是 'b' | 'd' 了。

Extract

Extract 的做用是提取出 T 包含在 U 中的元素,换种更加贴近语义的说法就是从 T 中提取出 U,源码以下:

/**
* Extract from T those types that are assignable to U
*/

type Extract<T, U> = T extends U ? T : never;
复制代码

Demo:

type T01 = Extract<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>;  // -> 'a' | 'c'
复制代码

Omit

PickExclude 进行组合, 实现忽略对象某些属性功能, 源码以下:

/**
* Construct a type with the properties of T except for those in type K.
*/

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
复制代码

Demo:

// 使用
type Foo = Omit<{name: string, age: number}, 'name'> // -> { age: number }
复制代码

更多工具泛型

其实经常使用的工具泛型大概就是我上面介绍的几种。更多的工具泛型,能够经过查看 lib.es5.d.ts里面查看。

毕竟。。。搬运几段声明着实没啥意思。

罗列 api 的写着也怪无聊的...
罗列 api 的写着也怪无聊的...

类型断言

断言这种东西仍是少用。。。。很少对于初学者,估计最快熟练掌握的就是类型断言了。毕竟 any 大法好

Typescript 容许咱们覆盖它的推断(毕竟代码使咱们本身写的),而后根据咱们自定义的类型去分析它。这种机制,咱们称之为 类型断言

const nealyang = {};
nealyang.enName = 'Nealyang'; // Error: 'enName' 属性不存在于 ‘{}’
nealyang.cnName = '一凨'; // Error: 'cnName' 属性不存在于 '{}'
复制代码
interface INealyang = {
enName:string;
cnName:string;
}

const nealyang = {} as INealyang; // const nealyang = <INealyang>{};
nealyang.enName = 'Nealyang';
nealyang.cnName = '一凨';
复制代码

类型断言比较简单,其实就是“纠正”ts对类型的判断,固然,是否是纠正就看你本身的了。

须要注意一下两点便可:

  • 推荐类型断言的预发使用 as关键字,而不是 <> ,防止歧义
  • 类型断言并不是类型转换,类型断言发生在编译阶段。类型转换发生在运行时

函数重载

在我刚开始使用 ts 的时候,我一直困惑。。。为何会有函数重载这么鸡肋的写法,可选参数它不香么?

慢慢你品
慢慢你品

函数重载的基本语法:

declare function test(a: number): number;
declare function test(a: string): string;

const resS = test('Hello World'); // resS 被推断出类型为 string;
const resN = test(1234); // resN 被推断出类型为 number;
复制代码

这里咱们申明了两次?!为何我不能判断类型或者可选参数呢?后来我遇到这么一个场景,

interface User {
name: string;
age: number;
}

declare function test(para: User | number, flag?: boolean): number;
复制代码

在这个 test 函数里,咱们的本意多是当传入参数 para 是 User 时,不传 flag,当传入 para 是 number 时,传入 flag。TypeScript 并不知道这些,当你传入 para 为 User 时,flag 一样容许你传入:

const user = {
name: 'Jack',
age: 666
}

// 没有报错,可是与想法违背
const res = test(user, false);
复制代码

使用函数重载能帮助咱们实现:

interface User {
name: string;
age: number;
}

declare function test(para: User): number;
declare function test(para: number, flag: boolean): number;

const user = {
name: 'Jack',
age: 666
};

// bingo
// Error: 参数不匹配
const res = test(user, false);
复制代码

Ts 的一些实战

我以前在公众号里面发表过两篇关于TS在实战项目中的介绍:

参考文献

学习交流

  • 关注公众号【全栈前端精选】,每日获取好文推荐
  • 添加微信号:is_Nealyang(备注来源) ,入群交流
公众号【全栈前端精选】 我的微信【is_Nealyang】