TypeScript在Vue中的应用

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

1.1 定义

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

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

1.2 基本要素

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

  • 对数重编码 在类型体操中,我们将数抽象为一种信息编码,通过元组的信息来体现数这一概念。
// 元组实现自然数的加减乘除
// 基本运算
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, []>
  • 数据类型与数据结构

    • 基本数据类型:数值字面量 和 字符串字面量;
    • 基本数据类型操作:自然数的加减乘除 和 模板字符串类型配合模式匹配;
    • 复合类型:主要是对象类型和元组类型,往往使用模式匹配来操作。
// 基本类型操作示例
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,"
  • 变量的声明与赋值
type A = 'hello';
type B = 'world' extends infer T ? (
    T
    // 声明局部变量
    // 在这个表达式的作用域内,T 都为 'world'
    // 类似函数式语言中 let in 的语法
) : never;
// 相对的常规 typescript 代码如下
let A = 'hello';
  • 函数的声明与调用
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 和 闭包特性,前者不影响编程语言的表达能力,后者可以通过将上下文(环境)当成函数的一个参数进行传递实现相同的逻辑,换句话说:闭包是一个在函数间隐式传递的参数,而我们只需要将隐式传递的闭包作为参数显式提取出来即可。
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 的逻辑
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 版本开始增加尾递归优化的支持

// 以斐波那契数列求值举例
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 这一插件来提供服务。

TypeScript在Vue中的应用

具体实现方法如下:

  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 职责分工

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

TypeScript在Vue中的应用

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率,接入性价比不好评价,根据每个团队水平不同而定。

原文链接:https://juejin.cn/post/7327253865358262324 作者:Saasfly技术出海小助手

(0)
上一篇 2024年1月24日 下午4:36
下一篇 2024年1月24日 下午4:46

相关推荐

发表回复

登录后才能评论