TypeScript在Vue中的应用

1、TypeScript 类型体操-快乐的屠龙术

1.1 定义

总的来说该部分将typescript的类型运算单独抽出来视为一种编程语言,我们可以称它为类型体操,给它一个简单的定义如下:

一种缺少 first-class function闭包特性的纯函数式编程语言。

1.2 基本要素

通过一些转换,它可以满足以下作为编程语言所需的基本要素:

  • 对数重编码 在类型体操中,我们将数抽象为一种信息编码,通过元组的信息来体现数这一概念。
scala 复制代码
// 元组实现自然数的加减乘除
// 基本运算
export type NArray<T, N extends number> = N extends N ? (number extends N ? T[] : _NArray<T, N, []>) : never;
type _NArray<T, N extends number, R extends unknown[]> = R['length'] extends N ? R : _NArray<T, N, [T, ...R]>;
type NArrayNumber<L extends number> = NArray<number, L>

// 加法
export type Add<M extends number, N extends number> = [...NArrayNumber<M>, ...NArrayNumber<N>] ['length']

// 减法
export type Subtract<M extends number, N extends number> =
    NArrayNumber<M> extends [...х: NArrayNumber<N>, ...rest: infer R] ? R['length'] : unknown

// 主要用于辅助推导乘除法,否则会因为 Subtract 返回类型为 number | unknown 报错
type _Subtract<M extends number, N extends number> =
    NArrayNumber<M> extends [...х: NArrayNumber<N>, ...rest: infer R] ? R['length'] : -1
// 乘法
type _Multiply<M extends number, N extends number, res extends unknown[]> =
    N extends 0 ? res['length'] :_Multiply<M, _Subtract<N, 1>, [...NArray<number, M>, ...res]>
export type Multiply<M extends number, N extends number> = _Multiply<M, N, []>

// 除法
type _DivideBy<M extends number, N extends number, res extends unknown []> =
    M extends 0 ? res ["length"] : _Subtract<M, N> extends -1 ? unknown : _DivideBy< _Subtract<M, N>, N, [unknown, ...res]>
export type DividedBy<M extends number, N extends number> = N extends 0 ? unknown : _DivideBy<M, N, []>
  • 数据类型与数据结构

    • 基本数据类型:数值字面量 和 字符串字面量;
    • 基本数据类型操作:自然数的加减乘除 和 模板字符串类型配合模式匹配;
    • 复合类型:主要是对象类型和元组类型,往往使用模式匹配来操作。
scala 复制代码
// 基本类型操作示例
type Test = Subtract<Add<1, 2>, 3>;  // 1 + 2 - 3 = 0

type Head<T extends string> = T extends `${infer head}${infer tail}` ? head : T;
type Test1 = Head<'abc'>  // => 'a'

// 复合类型操作示例
type AsString<T> = T extends string ? T : '';
type AsStringArray<T> = T extends string[] ? T : [];
type Split<T extends string> = T extends `${infer A},${infer B}` ? [A, ...Split<B>] : [];
type Join<Arr extends string[]> = Arr extends [infer A, ...infer B] ? `${AsString<A>},${Join<AsStringArray<B>>}` : '';
type Test2 = Join<Split<'1,2,3'>>;  //  => "1,2,"
  • 变量的声明与赋值
ini 复制代码
type A = 'hello';
type B = 'world' extends infer T ? (
    T
    // 声明局部变量
    // 在这个表达式的作用域内,T 都为 'world'
    // 类似函数式语言中 let in 的语法
) : never;
// 相对的常规 typescript 代码如下
let A = 'hello';
  • 函数的声明与调用
scala 复制代码
type Fun<A extends number, B extends string = 'hello'> = [A, B];
//    ⬆  ⬆           ⬆                          ⬆         ⬆
// 函数名  参数名     参数类型                      默认值      函数体
type Test3 = Fun<10, 'world'>;  // => [10, 'world]
// 相对的常规 typescript 代码如下
function Fun(A: number, B: string = 'hello') { return [A, B] }
//        ⬆            ⬆    ⬆        ⬆            ⬆
//      函数名         参数名参数类型    默认值         函数体
const Test = Fun(10, 'world');  // => [10, 'world]
  • 对上下文(闭包)的抽象 我们可知类型体操没有 first-class function 和 闭包特性,前者不影响编程语言的表达能力,后者可以通过将上下文(环境)当成函数的一个参数进行传递实现相同的逻辑,换句话说:闭包是一个在函数间隐式传递的参数,而我们只需要将隐式传递的闭包作为参数显式提取出来即可。
scala 复制代码
type Function<Env extends { A: string }, B extends string> = `${Env['A']}${B}}`;
type CreateFunctionEnv<A extends string> = { A: A };
type Env = CreateFunctionEnv<'A'>;
type Test4 = Function<Env, 'B'>;  // => 'AB'
// 相对的常规 typescript 代码如下
function createFunction(a) {
    return function func(b) {
        return a + b;
    }
}
const func = createFunction('a');
const res = func('a');  // => 'ab'
  • 控制代码逻辑的条件与分支 我们用 extends ? : 来类比三元表达式来实现 if-else 的逻辑
ini 复制代码
type Equal<T, U> = T extends U ? U extends T ? true : false : false;
type A = 1;
type B = 2;
type _ = Equal<A, B> extends true ? true : false;
// 相对的常规 typescript 代码如下
let a = 1;
let b = 2;
const _ = a === b ? true : false;
  • 循环、 递归 以及尾递归 计算机理论证明循环完全可以被递归取代,因此我们在类型体操中用递归来实现循环的效果; 尾递归:这是是在函数 Return 的时候调用自身的一种特殊的递归。它可以进行尾递归优化,防止爆栈;在尾递归优化加持下的尾递归基本等于循环;

TypeScript 在 4.5-beta 版本开始增加尾递归优化的支持

scala 复制代码
// 以斐波那契数列求值举例
type AsNumber<T> = T extends number ? T : 0;
type Fib<N extends number, N1 extends number, N2 extends number> =
    N extends 0 ? N1 : Fib<AsNumber<Subtract<N, 1>>, N2, AsNumber<Add<N1, 2>>>;
type Test5 = Fib<2,3,4>;  // => 5
// 相对的常规 typescript 代码如下
function fib(n: number, n1: number, n2: number) : number{
    if(n === 0) {
        return n1;
    }
    return fib(n - 1, n2, n1 + 2)
}
fib(2, 3, 4);  // => 5

1.3 小结

在通过让类型体操满足一门编程语言的基本要素的过程中,我们得到了一套公式框架,通过它咱们可以实现 Lisp 解释器,也可以将常规的 typescript 代码转换为类型体操代码,刀在手,如何使用便看操刀人了。在工程中的运用场景一般在需要对类型进行一些比较复杂的卡控

2、基于 TypeScript 的语言服务为你的 DSL 带来丝滑的开发体验

2.1 领域特定语言 DSL

首先我们要了解DSL是什么。它常用于解决少数领域的问题,咱们所知的 HTML 就属于此类。与之相对的是通用语言GPL,它用于解决多数领域问题的编程语言,咱们所知的JavaScript、C++、Go等就属于此类。

在前端领域,比较经典的模式实践为:GPL 做控制,DSL 做描述。按照 DSL 的特点,它又可以被分为以下三种类型:

  • 外部 DSL 不需要依赖 GPL 作为 host language,如 HTML、CSS 和 GraphQL 等。

  • 嵌入式 DSL 需要依赖 GPL 作为 host language,常表现为其某个语用子集,如 React Hooks 等,再广化一些其概念,utils 和组件库等也可以视为一种嵌入式 DSL。

  • 混合式 DSL 嵌入部分GPL,但有自己的独特语法,如 Vue SFC 和 MDX 等(后者也是我们本次需要举例的对象)。

2.2 使用 Volar 为 MDX 提供 TypeScript 语言服务支持

MDX = MarkDown + Extension,是典型的混合式 DSL,它在 Markdown 的基础上额外支持了 JSX,可以做到混排 Markdown 文本和可执行组件。但他在编码阶段只能提供代码高亮,无法基于语法和语义给予更为优秀的开发体验,因此我们可以先将 MDX 翻译成 TypeScript 代码,然后通过语言服务协议(Language Server Protocol)使用 TypeScript 的语言服务。在本例中使用了 Volar 这一插件来提供服务。

具体实现方法如下:

  1. 确定需要实现的 MDX 特性

  2. 翻译到 TypeScript,分派语言服务

  3. 生成 Source Map,将 MDX 的 range 和 代码段的 range 建立映射关系

  4. 将生成的 Source Map 和构造出的代码段提供给 Volar,使用虚拟文件进行处理

2.3 小结

本章主要讲述了使用 TypeScript 为其他 DSL 提供服务的实践经验,DSL翻译到 TypeScript 的方法适用于各种常见的DSL,但不适用于类型系统无法通过TypeScript类型检查器实现的DSL,如类型依赖(Dependent Type)。

3、Vue 中的 TypeScript 支持

3.1 一次春节活动中的回顾

快手一个前端开发团队接到了针对春节活动发起的一次大型需求,当时该团队开发涉及到 TypeScript,但有相当一部分的同学缺乏 TypeScript 的开发经验,为了尽可能高质量的交付成果,他们从以下两个方面入手:

3.1.1 职责分工

考虑本次需求的复杂性和规模,他们将职责分工进一步细化,设计出了如图示的分工图:

3.1.2 代码质量

提高代码质量的方法主要有以下两种途径:

人工方法:

  • Code Review

  • QA 工程师回归测试

机械方法:

  • Unit Test

  • E2E Test

  • Type Check

  • Lint

上述两种方法,人工方法成本较大,且不具稳定性,对 RD 和 QA 同学的要求很高;机械方法校验比较死板,很难比较轻松地制定个性化校验规则。因此该团队使用 Volar 提供 TypeScript 的语言服务,并在此基础上客制化了相关的校验规则。

3.2 模板语言类型检查

列举了演讲者维护 Volar 时,修复的一个 emit 类型检查失灵的 bug,具体代码可见:github.com

3.3 小结

本章分享了该团队在一次大型活动类需求中对 TypeScript 类型校验的实践,这个团队中接入 TypeScript 的硬卡控在前期增加了相当大的开发成本,但中后期有效降低了bug率,接入性价比不好评价,根据每个团队水平不同而定。

相关推荐
腾讯TNTWeb前端团队6 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom11 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom11 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom11 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom11 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom11 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试