【万字长文】深刻理解 Typescript 高级用法

做者: 韩念琪

欢迎转载或分享,转载请注明做者及出处!javascript

相关连接

写做初衷

最近刚入职新公司,正好部门每一个新人都须要作技术分享,这里借这个机会把本身的知识体系梳理一遍。css

关于标题

本来题目设定为是 Typescript 高级用法介绍,可是考虑掘金有不少关于 Typescript 高级用法 相关的分享,写的都很是好,这里就不作重复性工做了。因此把题目改为了 深刻理解 Typescript 高级用法,注重 理解 而非 使用html

正文

前言:这里的标题看起来是 "高级用法",很多同窗可能就表示被劝退了。其实 Typescript 做为一门 强类型 编程语言,最具特点的就是他的类型表达能力,这是不少完备的后端语言都难以媲美的 说的很对,但PHP是最好的语言,因此若是你搞懂了他的类型系统,对未来的平常开发必定是大有裨益的,但过于灵活的类型系统也注定了 Typescript 没法成为一门纯粹的静态语言,不过每一行代码都有代码提示他不香嘛?前端

为了更好的阅读体验,建议在宽屏浏览器端阅读。vue

大纲

基础准备

阅读本文须要具有的基础知识。java

预备知识

本文的定位为理解高级用法,故不会涉及过多基础知识相关的讲解,须要读者本身去完善这方面的知识储备。node

此文档的内容默认要求读者已经具有如下知识:
  1. Javascript 或其余语言编程经验。
  2. Typescript 实际使用经验,最好在正经项目中完整地使用过。
  3. 了解 Typescript 基础语法以及常见关键字地做用。
  4. Typescript类型系统 架构有一个最基本的了解。

相关资源推荐

  1. Typescript 官网
  2. TypeScript Deep Dive
  3. TypeScript GitHub地址

背景

初用 Typescript 开发的同窗必定有这样的困扰:react

  1. 代码代码提示并不智能,彷佛只能显式的定义类型,才能有代码提示,没法理解这样的编程语言竟然有这么多人趋之若鹜。
  2. 各类各样的类型报错苦不堪言,本觉得听信网上说 Typescript 能够提升代码可维护性,结果却发现徒增了很多开发负担。
  3. 显式地定义全部的类型彷佛能应付大部分常见,但遇到有些复杂的状况却发现无能为力,只能含恨写下若干的 as any 默默等待代码 review 时的公开处刑。
  4. 项目急时间紧却发现 Typescript 成了首要难题,思索片刻决定投靠的 Anyscript,快速开发业务逻辑,待到春暖花开时再回来补充类型。双倍的工做量,双倍的快乐只有本身才懂。

为了不以上悲剧的发生或者重演,咱们只有在对它有更加深入的理解以后,才能在开发时游刃有余、在撸码时纵横捭阖。jquery

Typescript 类型系统简述

思考题:有人说 Typescript = Type + Javascript,那么抛开 Javascript 不谈,这里的 Type 是一门完备的编程语言吗?webpack

Typescript 的类型是支持定义 "函数定义" 的

有过编程经验的同窗都知道,函数是一门编程语言中最基础的功能之一,函数是过程化、面向对象、函数式编程中程序封装的基本单元,其重要程度不言而喻。

函数能够帮助咱们作不少事,好比 :

  • 函数能够把程序封装成一个个功能,并造成函数内部的变量做用域,经过静态变量保存函数状态,经过返回值返回结果。
  • 函数能够帮助咱们实现过程的复用,若是一段逻辑能够被使用屡次,就封装成函数,被其它过程屡次调用。
  • 函数也能够帮咱们更好地组织代码结构,帮助咱们更好地维护代码。
那么言归正传,如何在 Typescript 类型系统中定义函数呢?

Typescript 中类型系统中的的函数被称做 泛型操做符,其定义的简单的方式就是使用 type 关键字:

// 这里咱们就定义了一个最简单的泛型操做符
type foo<T> = T;
复制代码

这里的代码如何理解呢,其实这里我把代码转换成你们最熟悉的 Javascript 代码其实就不难理解了:

// 把上面的类型代码转换成 `JavaScript` 代码
function foo(T) {
	return T
}
复制代码

那么看到这里有同窗内心要犯嘀咕了,心想你这不是忽悠我嘛?这不就是 Typescript 中定义类型的方式嘛?这玩意儿我可太熟了,这玩意儿不就和 interface 同样的嘛,我还知道 Type 关键字和 interface 关键字有啥细微的区别呢!

嗯,同窗你说的太对了,不过你不要着急,接着听我说,其实类型系统中的函数还支持对入参的约束。

// 这里咱们就对入参 T 进行了类型约束
type foo<T extends string> = T;
复制代码

那么把这里的代码转换成咱们常见的 Typescript 是什么样子的呢?

function foo(T: string) {
	return T
}
复制代码

固然啦咱们也能够给它设置默认值:

// 这里咱们就对入参 T 增长了默认值
type foo<T extends string = 'hello world'> = T;
复制代码

那么这里的代码转换成咱们常见的 Typescript 就是这样的:

function foo(T: string = 'hello world') {
	return T
}
复制代码

看到这里确定有同窗火烧眉毛地想要提问了:那能不能像 JS 里的函数同样支持剩余参数呢?

很遗憾,目前暂时是不支持的,可是在咱们平常开发中必定是有这样的需求存在的。那就真的没有办法了嘛?其实也不必定,咱们能够经过一些骚操做来模拟这种场景,固然这个是后话了,这里就不做拓展了。

Typescript 的类型是支持 "条件判断" 的

人生总会面临不少选择,编程也是同样。

——我瞎编的

条件判断也是编程语言中最基础的功能之一,也是咱们平常撸码过程成最经常使用的功能,不管是 if else 仍是 三元运算符,相信你们都有使用过。

那么在 Typescript 类型系统中的类型判断要怎么实现呢?

其实这在 Typescript 官方文档被称为 条件类型(Conditional Types),定义的方法也很是简单,就是使用 extends 关键字。

T extends U ? X : Y;
复制代码

这里相信聪明的你一眼就看出来了,这不就是 三元运算符 嘛!是的,并且这和三元运算符的也发也很是像,若是 T extends Utrue 那么 返回 X ,不然返回 Y

结合以前刚刚讲过的 "函数",咱们就能够简单的拓展一下:

type num = 1;
type str = 'hello world';

type IsNumber<N> = N extends number ? 'yes, is a number' : 'no, not a number';

type result1 = IsNumber<num>; // "yes, is a number"
type result2 = IsNumber<str>; // "no, not a number"
复制代码

这里咱们就实现了一个简单的带判断逻辑的函数。

Typescript 的类型是支持 "数据结构" 的

模拟真实数组

看到这里确定有同窗就笑了,这还不简单,就举例来讲,Typescript 中最多见数据类型就是 数组(Array) 或者 元组(tuple)

同窗你说的很对,那你知道如何对 元组类型pushpopshiftunshift 这些行为操做吗?

其实这些操做都是能够被实现的:

// 这里定义一个工具类型,简化代码
type ReplaceValByOwnKey<T, S extends any> = { [P in keyof T]: S[P] };

// shift action
type ShiftAction<T extends any[]> = ((...args: T) => any) extends ((arg1: any, ...rest: infer R) => any) ? R : never; // unshift action type UnshiftAction<T extends any[], A> = ((args1: A, ...rest: T) => any) extends ((...args: infer R) => any) ? R : never; // pop action type PopAction<T extends any[]> = ReplaceValByOwnKey<ShiftAction<T>, T>; // push action type PushAction<T extends any[], E> = ReplaceValByOwnKey<UnshiftAction<T, any>, T & { [k: string]: E }>; // test ... type tuple = ['vue', 'react', 'angular']; type resultWithShiftAction = ShiftAction<tuple>; // ["react", "angular"] type resultWithUnshiftAction = UnshiftAction<tuple, 'jquery'>; // ["jquery", "vue", "react", "angular"] type resultWithPopAction = PopAction<tuple>; // ["vue", "react"] type resultWithPushAction = PushAction<tuple, 'jquery'>; // ["vue", "react", "angular", "jquery"] 复制代码

注意:这里的代码仅用于测试,操做某些复杂类型可能会报错,须要作进一步兼容处理,这里简化了相关代码,请勿用于生产环境!

相信读到这里,大部分同窗应该能够已经能够感觉到 Typescript 类型系统的强大之处了,其实这里仍是继续完善,为元组增长 concatmap 等数组的经常使用的功能,这里不做详细探讨,留给同窗们本身课后尝试吧。

可是其实上面提到的 "数据类型" 并非我这里想讲解的 "数据类型",上述的数据类型本质上仍是服务于代码逻辑的数据类型,其实并非服务于 类型系统 自己的数据类型。

上面这句话的怎么理解呢?

无论是 数组 仍是 元组,在广义的理解中,其实都是用来对 数据批量操做,同理,服务于 类型系统 自己的数据结构,应该也能够对 类型批量操做

那么如何对 类型批量操做 呢?或者说服务于 类型系统 中的 数组 是什么呢?

下面就引出了本小节真正的 "数组":联合类型(Union Types)

提及 联合类型(Union Types) ,相信使用过 Typescript 同窗的必定对它又爱又恨:

  1. 定义函数入参的时候,当同一个位置的参数容许传入多种参数类型,使用 联合类型(Union Types) 会很是的方便,但想智能地推导出返回值的类型地时候却又犯了难。
  2. 当函数入参个数不肯定地时候,又不肯意写出 (...args: any[]) => void 这种毫无卵用的参数类型定义。
  3. 使用 联合类型(Union Types) 时,虽然有 类型守卫(Type guard),可是某些场景下依然不够好用。

其实当你对它有足够的了解时,你就会发现 联合类型(Union Types)交叉类型(Intersection Types) 不知道高到哪里去了,我和它谈笑风生

类型系统中的 "数组"

下面就让咱们更加深刻地了解一下 联合类型(Union Types):
如何遍历 联合类型(Union Types) 呢?

既然目标是 批量操做类型,天然少不了类型的 遍历,和大多数编程语言方法同样,在 Typescript 类型系统中也是 in 关键字来遍历。

type key = 'vue' | 'react';

type MappedType = { [k in key]: string } // { vue: string; react: string; }
复制代码

你看,经过 in 关键字,咱们能够很容易地遍历 联合类型(Union Types),并对类型做一些变换操做。

但有时候并非全部全部 联合类型(Union Types) 都是咱们显式地定义出来的。

咱们想动态地推导出 联合类型(Union Types) 类型有哪些方法呢?

可使用 keyof 关键字动态地取出某个键值对类型的 key

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

type studentKey = keyof Student; // "name" | "age"
复制代码

一样的咱们也能够经过一些方法取出 元组类型 子类型

type framework = ['vue', 'react', 'angular'];

type frameworkVal1 = framework[number]; // "vue" | "react" | "angular"
type frameworkVal2 = framework[any]; // "vue" | "react" | "angular"
复制代码

实战应用

看到这里,有的同窗可能要问了,你既然说 联合类型(Union Types) 能够批量操做类型,那我想把某一组类型批量映射成另外一种类型,该怎么操做呢

方法其实有不少,这里提供一种思路,抛砖引玉一下,别的方法就留给同窗们自行研究吧。

其实分析一下上面那个需求,不难看出,这个需求其实和数组的 map 方法有点类似

那么如何实现一个操做 联合类型(Union Types) 的 map 函数呢?
// 这里的 placeholder 能够键入任何你所但愿映射成为的类型
type UnionTypesMap<T> = T extends any ? 'placeholder' : never;
复制代码

其实这里聪明的同窗已经看出来,咱们只是利用了 条件类型(Conditional Types),使其的判断条件老是为 true,那么它就老是会返回左边的类型,咱们就能够拿到 泛型操做符 的入参并自定义咱们的操做。

让咱们趁热打铁,再举个具体的栗子:把 联合类型(Union Types) 的每一项映射成某个函数的 返回值

type UnionTypesMap2Func<T> = T extends any ? () => T : never;

type myUnionTypes = "vue" | "react" | "angular";

type myUnionTypes2FuncResult = UnionTypesMap2Func<myUnionTypes>;
// (() => "vue") | (() => "react") | (() => "angular")
复制代码

相信有了上述内容的学习,咱们已经对 联合类型(Union Types) 有了一个相对全面的了解,后续在此基础之上在做一些高级的拓展,也如砍瓜切菜通常简单了。

其余数据类型

固然除了数组,还存在其余的数据类型,例如能够用 typeinterface 模拟 Javascript 中的 字面量对象,其特征之一就是可使用 myType['propKey'] 这样的方式取出子类型。这里抛砖引玉一下,有兴趣的同窗能够自行研究。

Typescript 的类型是支持 "做用域" 的

全局做用域

就像常见的编程语言同样,在 Typescript 的类型系统中,也是支持 全局做用域 的。换句话说,你能够在没有 导入 的前提下,在 任意文件任意位置 直接获取到而且使用它。

一般使用 declare 关键字来修饰,例如咱们常见的 图片资源 的类型定义:

declare module '*.png';
declare module '*.svg';
declare module '*.jpg';
复制代码

固然咱们也能够在 全局做用域 内声明一个类型:

declare type str = string;
declare interface Foo {
  propA: string;
  propB: number;
}
复制代码

须要注意的是,如何你的模块使用了 export 关键字导出了内容,上述的声明方式可能会失效,若是你依然想要将类型声明到全局,那么你就须要显式地声明到全局:

declare global {
  const ModuleGlobalFoo: string;
}
复制代码

模块做用域

就像 nodejs 中的模块同样,每一个文件都是一个模块,每一个模块都是独立的模块做用域。这里模块做用域触发的条件之一就是使用 export 关键字导出内容。

每个模块中定义的内容是没法直接在其余模块中直接获取到的,若是有须要的话,可使用 import 关键字按需导入。

泛型操做符做用域&函数做用域

泛型操做符是存在做用域的,还记得这一章的第一节为了方便你们理解,我把泛型操做符类比为函数吗?既然能够类比为函数,那么函数所具有的性质,泛型操做符天然也能够具有,因此存在泛型操做符做用域天然也就很好理解了。

这里定义的两个同名的 T 并不会相互影响:

type TypeOperator<T> = T;
type TypeOperator2<T> = T;
复制代码

上述是关于泛型操做符做用域的描述,下面咱们聊一聊真正的函数做用域:

类型也能够支持闭包

function Foo<T> () {
  return function(param: T) {
    return param;
  }
}

const myFooStr = Foo<string>();
// const myFooStr: (param: string) => string
// 这里触发了闭包,类型依然能够被保留
const myFooNum = Foo<number>();
// const myFooNum: (param: number) => number
// 这里触发了闭包,类型也会保持相互独立,互不干涉
复制代码

Typescript 的类型是支持 "递归" 的

Typescript 中的类型也是能够支持递归的,递归相关的问题比较抽象,这里仍是举例来说解,同时为了方便你们的理解,我也会像第一节同样,把类型递归的逻辑用 Javascript 语法描述一遍。

首先来让咱们举个栗子:

假如如今须要把一个任意长度的元组类型中的子类型依次取出,并用 `&` 拼接并返回。

这里解决的方法其实很是很是多,解决的思路也很是很是多,因为这一小节讲的是 递归,因此咱们使用递归的方式来解决。废话不罗嗦,先上代码:

// shift action
type ShiftAction<T extends any[]> = ((...args: T) => any) extends ((arg1: any, ...rest: infer R) => any) ? R : never; type combineTupleTypeWithTecursion<T extends any[], E = {}> = { 1: E, 0: combineTupleTypeWithTecursion<ShiftAction<T>, E & T[0]> }[T extends [] ? 1 : 0] type test = [{ a: string }, { b: number }]; type testResult = combineTupleTypeWithTecursion<test>; // { a: string; } & { b: number; } 复制代码

看到上面的代码是否是一脸懵逼?不要紧,接下来咱们用普通的 Typescript 代码来 "翻译" 一下上述的代码。

function combineTupleTypeWithTecursion(T: object[], E: object = {}): object {
  return T.length ? combineTupleTypeWithTecursion(T.slice(1), { ...E, ...T[0] }) : E
}

const testData = [{ a: 'hello world' }, { b: 100 }];
// 此时函数的返回值为 { a: 'hello world', b: 100 }
combineTupleTypeWithTecursion(testData);
复制代码

看到这儿,相信聪明的同窗一会儿就懂了,原来类型的递归与普通函数的递归本质上是同样的。若是触发结束条件,就直接返回,不然就一直地递归调用下去,所传递的第二个参数用来保存上一次递归的计算结果。

固然熟悉递归的同窗都知道,常见的编程语言中,递归行为很是消耗计算机资源的,一旦超出了最大限制那么程序就会崩溃。同理类型中的递归也是同样的,若是递归地过深,类型系统同样会崩溃,因此这里的代码你们理解就好,尽可能不要在生产环境使用哈。

小结

还记得一开始提出的思考题吗?其实经过上述的学习,咱们彻底能够自信地说出,TypescriptType 自己也是一套完备的编程语言,甚至能够说是完备的图灵语言。所以类型自己也是能够用来编程的,你彻底能够用它来编写一些有趣的东西,更别说是搞定平常开发中遇到的简单的业务场景了。

"高级用法" 的使用场景与价值

哪些用法能够被称为 "高级用法"

其实所谓 "高级用法",不过是用来解决某些特定的场景而产生的特定的约定俗称的写法或者语法糖。那高级用法重要吗?重要,也不重要。怎理解呢,根据编程中的 "二八原则",20%的知识储备已经能够解决80%的需求问题,可是这剩余的20%,就是入门与熟练的分水岭。

其实只要当咱们仔细翻阅一遍官方提供的 handbook,就已经能够应付平常开发了。可是就像本文一开头说的那样,你是否以为:

  1. Typescript 在某些场景下用起来很费劲,远不及 Javascript 灵活度的十分之一。
  2. 你是否为本身使用 Javascript 中了某些 骚操做 用极简短的代码解决了某个复杂的代码而沾沾自喜,但却为不正确的 返回类型 挠秃了头。
  3. 你是否明知用了若干 as xxx 会让你的代码看起来很挫,但却无能为力,含恨而终。

同窗,当你使用某种办法解决了上述的这些问题,那么这种用法就能够被称做 "高级用法"。

举例说明 "高级用法" 的使用场景

举个栗子:在 Redux 中有一个叫做 combineReducers 的函数,由于某些场景,咱们须要增长一个 combineReducersParamFactory 的函数,该函数支持传入多个函数,传入函数的返回值为做为combineReducers 的入参,咱们须要整合多个入参数函数的返回值,并生成最终的对象供 combineReducers 函数使用。

思考一下逻辑,发现其实并不复杂,用 Javascript 能够很容易地实现出来:

/** * 合并多个参数的返回数值并返回 * @param { Function[] } reducerCreators * @returns { Object } */
function combineReducersParamFactory(...reducerCreators) {
  return reducerCreators.reduce((acc, creator) => ({ ...acc, ...creator() }), {})
}

// test ...

function todosReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}

function counterReducer(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

const ret = combineReducersParamFactory(
  () => ({ todosReducer }),
  () => ({ counterReducer })
);
// { todosReducer: [Function: todosReducer], counterReducer: [Function: counterReducer] }
复制代码

但若是用须要配备对应的类型,应该如何编写呢?

type Combine<T> = (T extends any ? (args: T) => any : never) extends (args: infer A) => any ? A : never;

/**
 * 合并多个参数的返回数值并返回
 * @param { Function[] } reducerCreators
 * @returns { Object }
 */
function combineReducersParamFactory<T extends ((...args) => object)[]>(...reducerCreators: T): Combine<ReturnType<T[number]>> {
  return reducerCreators.reduce<any>((acc, creator) => ({ ...acc, ...creator() }), {});
}

// test ...

function todosReducer(state: object[], action: { [x: string]: string}) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}

function counterReducer(state: number, action: { [x: string]: string}) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

// 这里不须要显示传入类型,这里就能够获得正确的代码提示
const ret = combineReducersParamFactory(
  () => ({ todosReducer }),
  () => ({ counterReducer })
);
// { todosReducer: [Function: todosReducer], counterReducer: [Function: counterReducer] }
复制代码

你看,类型通过精心编排以后,就是可让调用者不增长任何负担的前提下,享受到代码提示的快乐。

小结

通过这一章节的学习,咱们能够明确了解到,通过咱们精心编排的类型,能够变得很是的智能,可让调用者几乎零成本地享受到代码提示的快乐。或许在编排类型时所耗费的时间成本比较大,可是一旦咱们编排完成,就能够极大地减小调用者的脑力负担,让调用者享受到编程的快乐。

类型推导与泛型操做符

流动的类型(类型编写思路)

熟悉 函数式编程 的同窗必定对 数据流动 的概念有较为深入的理解。当你在 "上游" 改变了一个值以后,"下游" 相关的会跟着自动更新。有 响应式编程 经验的同窗这是时候应该火烧眉毛地想举手了,同窗把手放下,这里咱们并不想深刻地讨论 流式编程思想,之因此引出这些概念,是想类比出本小节的重点: 流动的类型

是的,编写类型系统的思路是能够借鉴 函数式编程 的思想的。所以某一个类型发生变化时,其余相关的类型也会自动更新,而且当代码的臃肿到不可维护的时候,你会获得一个友好的提示,整个类型系统就好像一个被精心设计过的约束系统。

Typescript 代码哲学

聊完了类型系统的编写思路,我们再来聊一聊代码哲学。其实之因此如今 Typescript 愈来愈火,撇开哪些聊烂了的优点不谈,其实最大的优点在于强大的类型表现能力,以及编辑器(VSCode)完备的代码提示能力。

那么在这些优点的基础上,我我的拓展了一些编码哲学(习惯),这里见仁见智,大佬轻喷~:

  1. 减小没必要要的显式类型定义,尽量多地使用类型推导,让类型的流动像呼吸同样天然。
  2. 尽量少地使用 anyas any,注意这里并非说不能用,而是你判断出目前状况下使用 any 是最优解。
  3. 若是肯定要使用 any 做为类型,优先考虑一下是否可使用 unknown 类型替代,毕竟 any 会破坏类型的流动。
  4. 尽量少地使用 as xxx,若是大量使用这种方式纠正类型,那么大几率你对 类型流动 理解的还不够透彻。

常见类型推导实现逻辑梳理与实践入门

类型的传递(流动)

前面咱们说到,类型是具有流动性的,结合 响应式编程 的概念其实很容易理解。这一小节咱们将列举几个常见的例子,来和你们具体讲解一下。

有编程经验的同窗都知道,数据是能够被传递的,同理,类型也能够。

你可用 type 建立一个类型指针,指向对应的类型,那么就能够实现类型的传递,固然你也能够理解为指定起一个别名,或者说是拷贝,这里见仁见智,可是经过上述方法能够实现类型的传递,这是显而易见的。

type RawType = { a: string, b: number };

// 这里就拿到了上述类型的引用
type InferType = RawType; // { a: string, b: number };
复制代码

一样,类型也能够随着数据的传递而传递:

var num: number = 100;
var num2  = num;

type Num2Type = typeof num2; // number
复制代码

也正是依赖这一点,Typescript 才得以实现 类型检查定义跳转 等功能。

到这里熟悉 流式编程 的同窗就要举手了:你光说了类型的 传递输入输出,那我若是但愿在类型 传递 的过程当中对它进行操做,该怎么作呢?同窗你不要急,这正是我下面所想要讲的内容。

类型的过滤与分流

在上一小节中,咱们反复地扯到了 函数式编程响应式编程流式编程 这些抽象的概念,其实并非跑题,而是者二者的思想(理念)实在太类似了,在本小节后续的讲解中,我还会一直延用这些概念帮助你们理解。翻看一下经常使用 函数式编程 的库,无论是 RamdaRXJS 仍是咱们耳熟能详的 lodashunderscore,里面必定有一个操做符叫做 filter,也就是对数据流的过滤。

这个操做符的使用频率必定远超其余操做符,那么这么重要的功能,咱们在类型系统中该如何实现呢?

要解决这个问题,这里咱们先要了解一个在各大 技术社区/平台 搜索频率很是高的一个问题:

TypeScript中 的 never 类型具体有什么用?

既然这个问题搜索频率很是之高,这里我也就不重复做答,有兴趣的同窗能够看一下尤大大的回答: TypeScript中的never类型具体有什么用? - 尤雨溪的回答 - 知乎

这里咱们简单总结一下:

  1. never 表明空集。
  2. 经常使用于用于校验 "类型收窄" 是否符合预期,就是写出类型绝对安全的代码。
  3. never 常被用来做 "类型兜底"。

固然上面的总结并不完整,但已经足够帮助理解本小节内容,感兴趣的同窗能够自行查阅相关资料。

上面提到了 "类型收窄",这与咱们的目标已经十分接近了,固然咱们还须要了解 never 参与类型运算的相关表现:

type NeverTest = string | never // stirng
type NeverTest2 = string & never // never
复制代码

重要的知识出现了:T | never,结果为 T

看到这里,相信聪明的同窗们已经有思路了,咱们能够用 never 来过滤掉 联合类型(Union Types) 中不和指望的类型,其实这个 泛型操做符 早在 Typescript 2.8 就已经被加入到了官方文档中了。

/** * Exclude from T those types that are assignable to U */
type Exclude<T, U> = T extends U ? never : T;
复制代码

相信通过这么长时间的学习,看到这里你必定很容易就能这种写法的思路。

好了,讲完了 过滤,咱们再来说讲 分流。类型 分流 的概念其实也不难理解,这个概念经常与逻辑判断一同出现,毕竟从逻辑层面来说,联合类型(Union Types) 本质上仍是用来描述 的关系。一样的概念若是引入到 流式编程 中,就天然而然地会引出 分流。换成打白话来说,就是不一样数据应被分该发到不一样的 管道 中,同理,类型也须要。

那么这么经常使用的功能,在 Typescript 中如何处理呢?其实这种常见的问题,官方也很是贴心地为咱们考虑到了,那就是:类型守卫(Type guard)。网上对 类型守卫(Type guard) 有讲解的文章很是的多,这里也不做赘述,有兴趣的同窗能够自行搜索学习。咱们这里用一个简单的栗子简单地演示一下用法:

function foo(x: A | B) {
  if (x instanceof A) {
    // x is A
  } else {
		// x is B
  }
}
复制代码

能够触发类型守卫的常见方式有typeofinstanceofin=====!=!== 等等。

固然在有些场景中,单单经过以上的方式不能知足咱们的需求,该怎么办呢?其实这种问题,官方也早已经帮我考虑到了:使用 is 关键字自定义 类型守卫(Type guard)

// 注意这里须要返回 boolean 类型
function isA(x): x is A {
  return true;
}

// 注意这里须要返回 boolean 类型
function isB(x): x is B {
  return x instanceof B;
}

function foo2(x: unknown) {
  if (isA(x)) {
    // x is A
  } else {
    // x is B
  }
}
复制代码

小结

这一章节中,咱们经过类比 响应式编程流式编程 的概念方式,帮助你们更好地理解了 类型推导 的实现逻辑与思路,相信通过了这一章节的学习,咱们对 Typescript 中的类型推导又有了更加深刻的理解。不过这一章引入的抽象的概念比较多,也比较杂,基础不是太好的同窗须要多花点时间翻看一下相关资料。

【未完待续...】常见泛型操做符实现逻辑梳理与实践入门

定制化扩展你的 Typescript

Typescript Service Plugins 的产生背景、功能定位、基础使用

产生背景

提及 Typescript 的编译手段大部分同窗应该都不会陌生,不管是在 webpack 中使用 ts-loaderbabel-loader,仍是在 gulp 中使用 gulp-typescript,亦或是直接使用 Typescript 自带的命令行工具,相信大部分同窗也都已经得心应手了,这里不作赘述。

这里咱们把目光聚焦到撸码体验上,相信有使用过 Typescritp 开发前端项目的同窗必定有过各类各样的困扰,这里列举几个常见的问题:

  1. 在处理 CSS Module 的样式资源的类型定义时,不知足于使用 declare module '*.module.css' 这种毫无卵用的类型定义。
  2. 不想给编辑器安装各类各样的插件,下次启动编辑器的时间明显变长,小破电脑不堪重负,并且每次重装系统都是一次噩梦降临。
  3. 不想妥协于同事的使用习惯,想使用本身熟悉的编辑器。
  4. 并不知足于官方已有的代码提示,想让本身的编辑器更加地贴心与智能。
为了提供更加贴心的开发体验,Typescript 官方提供一种解决思路——Typescript Service Plugins

功能定位

如下内容摘自官方 WIKI:

In TypeScript 2.2 and later, developers can enable language service plugins to augment the TypeScript code editing experience.

其实官方文档已经写的很清楚了,这玩意儿旨在优化 Typescript 代码的 编写体验。因此想利用这玩意儿改变编译结果或是想自创新语法的仍是省省吧 嗯,我在说我本身呢

那么 Typescript Service Plugins 的能够用来作哪些事呢?

官方也有明确的回答:

plugins are for augmenting the editing experience. Some examples of things plugins might do:

  • Provide errors from a linter inline in the editor
  • Filter the completion list to remove certain properties from window
  • Redirect "Go to definition" to go to a different location for certain identifiers
  • Enable new errors or completions in string literals for a custom templating language

一样官方也给出了不推荐使用 Typescript Service Plugins 的场景:

Examples of things language plugins cannot do:

  • Add new custom syntax to TypeScript
  • Change how the compiler emits JavaScript
  • Customize the type system to change what is or isn't an error when running tsc

好了,相信读到这里你们必定对 Typescript Service Plugins 有了一个大体的了解,下面我会介绍一下 Typescript Service Plugins 的安装与使用。

如何安装以及如何配置 Typescript Service Plugins

Typescript Service Plugins 的安装方法
# 就像安装普通的 `npm` 包同样
npm install --save-dev your_plugin_name
复制代码
如何在 tsconfig.json 中配置 Typescript Service Plugins
{
  "compilerOptions": {
    /** compilerOptions Configuration ... */
    "noImplicitAny": true,
    "plugins": [
      {
        /** 配置插件名称,也能够填写本地路径 */
        "name": "sample-ts-plugin"
        /** 这里能够给插件传参 ... */
      }
      /** 支持同时引入多个插件 ... */
    ]
  }
}
复制代码
几个须要注意的地方:
  1. 若是使用 VSCode 开发,记得务必 using the workspace version of typescript,不然可能致使插件不生效。
  2. Typescript Service Plugins 产生的告警或者报错不会影响编译结果。
  3. 若是配置完了不生效能够先尝试重启你的编辑器。

市面上已有的 Typescript Service Plugins 举例介绍

具体使用细节请用编辑器打开我提供的 demo,自行体验。

示例插件:typescript-plugin-css-modules

插件安装
npm install --save-dev typescript-styled-plugin typescript
复制代码
配置方法
在 tsconfig.json 中增长配置
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-styled-plugin"
        /** 具体配置参数请查看官方文档 */
      }
    ]
  }
}
复制代码
插件基本介绍与使用场景

此插件能够用来缓解在使用 CSS Module 时没有代码提示的困境,主要思路就是经过读取对应的 CSS Module 文件并解析成对应的 AST,并生成对应的类型文件从而支持对应的代码提示。可是根据反馈来看,彷佛某些场景下表现并不尽人意,是否值得大规模使用有待商榷。

相似实现思路的还有 typings-for-css-modules-loader,功能来讲确定是 webpack loader 更增强大,可是 Typescript Plugin 更加轻量、入侵度也越低,取舍与否,见仁见智吧

示例插件:typescript-eslint-language-service

插件安装
npm install --save-dev eslint typescript-eslint-language-service
复制代码
配置方法

.eslintrc.* 文件中,添加对应的 eslint 配置

在 tsconfig.json 中增长配置
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-eslint-language-service"
        /** 默认会读取 `.eslintrc.*` 文件 */
        /** 具体配置参数请查看官方文档 */
      }
    ]
  }
}
复制代码
插件基本介绍与使用场景

此插件可让 Typescript 原生支持 eslint 检查及告警,编辑器不须要安装任何插件便可自持,可是报错并不影响编译结果。

示例插件:typescript-styled-plugin

插件安装
npm install --save-dev typescript-styled-plugin typescript
复制代码
配置方法
在 tsconfig.json 中增长配置
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-styled-plugin"
        /** 具体配置参数请查看官方文档 */
      }
    ]
  }
}
复制代码
插件基本介绍与使用场景

此插件能够为 styled-components 的样式字符串模板提供 属性/属性值 作语法检查。 同时也推荐安装 VSCode 插件 vscode-styled-components,为你的样式字符串模板提供代码提示以及语法高亮。

如何本身写一个 Typescript Service Plugins

未完待续...

参考资料连接

  1. Using the Compiler API
  2. Using the Language Service API
  3. Writing a Language Service Plugin
  4. Useful Links for TypeScript Issue Management

总结

未完待续...

Q&A(欢迎评论区补充)

能够利用 Typescript Service Plugin(例如配置 eslint 规则)阻塞编译或者在编译时告警吗?

:不能够,全部可使用 Typescript Plugin 的场景必定都是编码阶段的,并且官方对 plugins 的定位局限在了 只改善编写体验 这方面,你并不能自定义语法或者自定义规则来改变编译结果,不过你能够考虑使用自定义 compiler,固然这是另外一个话题了。

如下引用自官方文档:

TypeScript Language Service Plugins ("plugins") are for changing the editing experience only. The core TypeScript language remains the same. Plugins can't add new language features such as new syntax or different typechecking behavior, and plugins aren't loaded during normal commandline typechecking or emitting.

相关文章
相关标签/搜索