2023 GMTC TypeScript 分享

2023GMTCopen in new window

TypeScript 对现代前端工程带来的优势

TypeScriptJavaScript
代码量增加:需要编写类型标注不需要额外的类型标注,JSDoc除外
开发效率代码补全,开发体验增加无编类型提示,无代码补全
工程质量类型约束,工程质量增加懂的都懂

TS 带来的开发体验

  • 实时类型检查
  • 实时代码补全
  • 快速代码重构
  • 嵌入类型的提示

Vue 中的 TypeScript 支持

Vue2 中 ts 的支持

Vue2.x 的类型都是基于 flow 来实现的 Vue 2 中全部属性都挂载在 this 之上,而 this 可以说是一个黑盒子,我们完全没办法预先知道 this 上会有什么数据,这也是为什么 Vue 2 对 TypeScript 的支持一直不太好的原因

Vue3 中 ts 的支持

Vue 3 全面拥抱 Composition API 之后,没有了 this 这个黑盒,对 TypeScript 的支持也比 Vue2 要好很多

如何拥有更好的 vue 开发体验?

Volar

Volar 从一个 vue vscode extension 到 embedded language tooling framework

Volar 实现了什么功能

代码高亮、vue 语法支持、代码补全、错误诊断、嵌入式语言(embedded language)...

Vue SFC

<template>
  <div>hello vue</div>
</template>
<script setup ts="lang">
import { ref } from 'vue'
</script>
<style lang="less" scoped>
body {
  font-size: 12px
}
</style>

Volar 的背后 TS 语言服务(Language Server)

  • TS 约等于 JS + 静态类型系统
  • TS 语言服务基于静态类型系统为编辑器提供了上面的各种功能
  • 语言服务是指对编辑器(vscode)提供语言支持,提供开发体验
  • 语言服务协议(Language Server Protocol)一种开源的语言服务器协定,各个编辑器只要支持LSP 就能接入语言服务

LSP 是微软定义的一套协议,本质上语言服务插件是一种特殊的 vs code 插件,目的是为了提升编程语言的开发体验和开发效率,比如提供了自动补全,错误检查,定义跳转,重命名等...   比如说:你想要 TS 在 Atom 编辑器里进行代码提示,就得单独针对 Atom 开发一套,想要在针对 Vim 实用 TS 类型补全,还要在开发一套。而且 Language Server 做各种提示的时候,需要解析大量文件,这种操作可能会消耗大量 CPU 资源,所以 LSP 出现了。LSP 标准化了 GPL 与代码编辑器之间的通信。 遵循LSP协议的插件有两个部分:

  1. 客户端(用JS/TS编写的VSCode插件)。 用来和 vs code 交互,是 vs code 与 LS 之间沟通的桥梁。客户端通过调用 vs code api,来监听 vs code 传过来的事件,或者向 vs code 发送消息
  2. 语言服务器(单独在进程中运行的语言分析工具):语言特性的核心,用来实现对文本,词法,语法分析,单独运行,不限于任何语言。

遵循 LSP 协议的好处就是 任何符合规范的 LSP 语言工具都可以与多个支持 LSP 的代码编辑器集成,也就不限于任何编辑器和语言, 比如 PHP https://github.com/felixfbecker/php-language-serveropen in new window扩展:

  1. CDP(Chrome DevTools Protocol) 实现 Chrome 调试功能
  2. _Vue Devtool _
  3. Debug Adapter Protocol 是基于 vs code 的一种调试通信协议,比如通过 vscode 调试 nodejs,前端web,Java 或者 Golang,都会遵循 DAP 协议。
  4. 基于 LSP 的 vue 插件

image.png

前端DSL

DSL 概念

一种专注于某个特定领域并具有有限表达性的计算机编程语言。 最成功的 DSL 代表: SQL、Regex

DSL 带来了什么

  1. 提升程序员开发效率:擅长在程序中某些特定部分发挥作用,让它们易于被理解,提高代码编写效率,减少bug
  2. 增进与领域专家的沟通:DSL 跳脱了程序员的范畴,DSL可以让非程序员看懂驱动业务的代码,然后就可以跟某些领域的专家进行沟通

前端的 DSL

  • 前端领域的经典实践:GPL做控制,DSL做描述
  • JavaScript做逻辑控制,HTML和CSS做文档结构和样式描述
  • React、Vue:TypeScript,JSX,SFC
  • ...
  • 外部 DSL:不以 JS 为宿主的特殊语言,如:HTML, CSS, GraphQL
  • 嵌入式 DSL: 以 JS 为宿主的:React Hooks, 广义上指的通用的 utils、组件库
  • 混合式 DSL: Vue SFC, MDX...

基于 TS 语言服务实现 DSL 开发

MDX
  • MDX = Markdown + Extension,混合式的 DSL
  • 支持 JSX
  • 支持 Vue, React 组件等

基于 VitePress 或者 VuePress 搭建的 SSG 都支持 MDX。 看个例子: 引用open in new window 组件open in new window

TypeScript 类型体操(类型编程)

什么是类型体操

利用 TS 类型运算 实现复杂功能的智力游戏

类型体操天花板

为什么需要类型编程?

int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

问: 如果 add 函数需要 float + float 呢? 再新增一个函数吗?

另一种类型

范型 T

TS 中的类型运算符

  • 条件: extends ? : type isBool = T extends boolean ? true : false
  • 推导:infer type PickFirst<Tuple extends unknown[]> = Tuple extends [infer T, ...inferR] ? T : never
  • 交叉:& type Obj = {a: number} &{b: string}
  • 联合:|type Union = 1 | 2 | 3
  • 映射:type MapType<T> = { [key in keyof T]?: T[Key]}

图灵完备

TypeScript 的类型系统是图灵完备的,也就是能描述各种可计算逻辑。简单点来理解就是循环、条件等各种 JS 里面有的语法它都有,JS 能写的逻辑它都能写

TS 类型运算是不是纯函数式编程语言?

是但又不完全是 函数是编程语言两个显著的特点:

  1. first-class-function(头等函数),即函数是一等公民,函数可以当作入参,可以当作返回值
  2. 闭包(Contenx/Environment)

因为缺少这两大特性,可以说 TS类型运算是一门独立的纯函数式编程语言

TS extends 与 GPL 的条件分支逻辑

extends 运算符: A extends B ? C : D -> 代表了如果 A 是 B 的子集,就返回 C, 否则返回 D。 extends 运算的两个用途:

  • <值> extends <值> ? :判断两个值是否相等,可以类比 javascript 的 <值> === <值> ? :
    • T extends 'foo'
    • 'foo' extends 'foobar'
  • <值> extends <类型> ? : 判断一个值是否属于某个类型,可以类比成 typeof <值> === <类型> ? :
    • T extends string
    • 'foo' extends string

变量

  1. type T = 'hello' // 全局变量,ts文件作用域
  2. type T = A extends infer B // 布局变量 => let B = A in xxx
type A = 'hello'; // 声明全局变量
type B = [A] extends infer T ? (
    T // => 在这个表达式的作用域内,T 都为 [A]
) : never  // 声明局部变量

函数调用

type Func<A extends number, B extends string = 'hello'> = [A, B]
//     ↑  ↑           ↑     ↑           ↑        ↑         ↑
// 函数名 参数名    参数类型  参数名       参数类型  默认值       函数体

type Test = Func<10, 'world'> // => [10, 'world']

function Func(A: number, B: string = 'hello') { return [A, B] }
const Test = Func(10, 'world') // => [10, 'world']
type GetParameters<Func extends Function> = 
    Func extends (...args: infer Args) => unknown ? Args : never;

type SayHello = (h: 'hello') => h

type GetSayHello = GetParameters<SayHello> // [h: 'hello']

递归代替循环

函数式编程中经常使用递归代替逻辑循环,同样在TS 类型编程种 可以用递归实现类型的循环条件

type TKeyToCamelCase<Str extends string> = 
    Str extends `${infer Item}_${infer Rest}` 
        ? `${Item}${TkeyToCamelCase<Capitalize<Rest>>}`
        : Str
type A = TKeyToCamelCase<'aa_bb_cc_dd'> //type A = "aaBbCcDd"

数值运算

没有加减乘除怎么做数值运算? 可以通过数组类型的 lenght 属性,length 属性就是个数值 type len = [unknown]['length'] // type len = 1

实现两个数加法

type Add<1, 2> => type Add === 3

// 第一步 构造一个元组
// 为什么是元组而不是数组是因为 [unknown, unknown]['lenght'] => 2
// unknown[]['length'] => number
type BuildArray<
    Len extends number,
    EleType = unknown,
    Arr extends unknown[] = []
> =  Arr['length'] extends Len 
? Arr 
: BuildArray<Len, EleType, [...Arr, EleType]>

type R = BuildArray<3> // [unknown, unknown, unknown]

// 第二步实现 Add
type Add<N1 extends number, N2 extends number> = 
  [...BuildArray<N1>, ...BuildArray<N2>]['length']

type ResultIs5 = Add<2, 3>
// 实现减法
type Subtract<N1 extends number, N2 extends number> = 
    BuildArray<N1> extends [...n1: BuildArray<N2>, ...n2: infer R] ? R['length'] : never

type SubtractR = Subtract<5, 2>

// 乘法
type Mutiply<
    N1 extends number,
    N2 extends number,
    ResultArr extends unknown[] = []
> = N2 extends 0 ? ResultArr['length']
        : Mutiply<1, Subtract<N2, 1>, [...BuildArray<N1>, ...ResultArr]>;
// 除法
type Divide<
    N1 extends number,
    N2 extends number,
    CountArr extends unknown[] = []
> = N1 extends 0 ? CountArr['length']
        : Divide<Subtract<N1, Num2>, N2, [unknown, ...CountArr]>;

TS 类型编程闭包的抽象

function CreateFunc(a) {
  return function func(b) {
    return a + b
  }
}

const func = CreateFunc('a')
const r = func('b') // r === 'ab'
type Func<
    Env extends {A: string},
    B extends string
    > = `${Env['A']}${B}`
type CreateFuncEnv<A extends string> = {A: A}

// ('A' extends string) === true
// { A: 'A' }
type Env = CreateFuncEnv<'A'>
type R = Func<Env, 'B'> // R => 'AB'

个人 TS 学习指北

  1. 基础语法
  2. 内置工具类型
  3. 类型体操
  4. 元编程:reflect-metadata、Reflect
  5. 工程化: tsconfig
  6. 面相对象: 面相对象 -> 面相接口
  7. AST 编程 ts-morphopen in new window 可以让操作 TS ast 很容易

TS 开发环境推荐

  • es-no 类似 ts-node, 用来执行ts文件,底层基于Esbuild
  • tsx 同上,底层基于 Esbuild
  • ts-node-dev, ts-node + nodemon 的结合体
  • tsd: npm library,帮助更好的进行声明式类型检查
  • TypeScript Importeropen in new window vs-code 插件,帮助类型补全
  • vs-code 开启 TypeScript 配置
  • Playgroundopen in new window 官方在线的 TS 环境,可实时查看编译后的 JS

附录

type challengeopen in new window