面试官:你说你会玩TS类型体操,那你能把类型打印出来吗?

注:如果你只想查看如何实现“打印类型”,请跳转到最后一章,如果你觉得有用的话,就给我一个免费的赞吧

面试官:听说你上次写了一个 TypeScript Lodash 库,但是好像没什么用吧

:对,上次我写 TypeScript Lodash 库 还是在上次

面试官:???

:其实里面的大部分都很难用到,但是它能提升我们对 TS 用法的认知程度,特定场景下也很有用,比如对对象类型的一些操作

面试官:那看起来 TS 类型体操并没有很有用呢?**对初学者来说,经常把一个类型当做一个 变(常)量/值 来使用,于是写出了如下代码,你能通过某些特殊的操作,让这段代码能够成功执行吗?**如果你可以解决这个问题,我就认为你的 TS 知识能够通过我的面试

type A = "111"

console.log(A)
复制代码

:这个问题对应的需求场景不常见,我来试试...

在回答这个问题之前,我们需要对 TS 的源码/原理有基本的认识

然后要先回答这三个问题:

  1. TS 中的类型是什么?
  2. TS 中的类型为什么不能当做值来使用?
  3. TS 中是如何处理像“类型不能当一个值来使用”的错误的?

如何阅读 TS 源码?

如何拉取 TS 源代码并编译?

关于 TS 源码的阅读和调试可以参考这篇文章我读 Typescript 源码的秘诀都在这里了

使用 git 拉取 TS 源代码(注意,源代码超过 1 个 G)

git clone https://github.com/microsoft/TypeScript.git
复制代码

进入生成的文件夹 TypeScript,装包

yarn
复制代码

编译

yarn run build:compiler
复制代码

你能用自己编译好的 TS 源码做些什么

VSCode TS 服务

VSCode 中的 TS 语言服务是可以自定义的

vscode-typescript-version.jpg

如果你当前编辑的是一个 TS 文件,在上图红框所示的位置(看到你 VSCode 的右下角),可以设置 TS 的版本

typescript-version-dialog.jpg

使用 VS Code 的版本,即使用 VS Code 自己用的版本,一般为最新的稳定版本的 TypeScript 语言服务,不需要额外的安装

如果你在你的项目的 node_modules 中安装了 typescript,那么会像我一样出现另一个选项,使用工作区版本,一般而言,你最好使用工作区的版本,让 VSCode 中的语言服务与你的项目一致,否则,有可能出现代码提示编译/类型检查结果不一致的情况

除了上述两种选项,还有一个隐藏选项,参考 VSCode 文档-Using_newer_TypeScript_versions

你可以在 settings.json 文件中配置 typescript.tsdk 选项

在项目中的 .vscode 文件夹中,新建 settings.json 文件

刚刚我们已经对我们使用 git 拉下来的 TS 源码进行了编译,你会发现生成了一个 built/local 文件夹

那么我们可以把这个设置为 VSCode TS 服务

在上述 settings.json 文件中,增加配置

{
  "typescript.tsdk": "./TypeScript/built/local/tscserver.js"
}
复制代码

version-type-custom.jpg

这样,你就可以魔改 TS 源码,并应用到你的编辑器啦

选择我们刚刚自定义的工作区版本,然后你会发现 VSCode 自动找到了这个目录下的 tsserver.js 文件,这个文件是干啥的呢?让我们在下文进行讲解

其他

使用它进行 debug,学习源码的执行流程

TS 源码的目录结构和对应文件的用处

ts-source-code-dirs.jpg

  • bin 目录:在一些基于 node 的命令行工具中很常见,在 package.json 中的 bin 属性中设置命令及其指向 bin 目录的路径,即可在终端使用该命令,或者配置与 package.json 的 scripts 属性中;在 TS 源码的 bin 目录中,有 tsctsserver 两个文件,我们平时用的 tsc ./index.ts 就是执行的 tsc 文件

  • built 目录:编译产物

  • doc 目录:包含各种文档,如 TypeScript 语言规范和徽标,此目录的文件是过时的规范的快照,如果要看文档,请去TypeScript Handbook

  • lib 目录:一些声明文件,你平常使用的 HTMLDivElement 类型,就是这里面定义的

  • loc 目录:全名 localize,即国际化,最后会被编译到 built/local/{语言 如zh-cn}/diagnosticMessages.generated.json 文件中,如上述的例子 type A = '111'; console.log(A)console.log 中 Token A 处的报错代码是 2693,那么我们在 built/local/zh-cn/diagnosticMessages.generated.json 文件中搜索到对应的文案提示为 “{0}”仅表示类型,但在此处却作为值使用。,你可以通过这个文件学习 TS 所有能抛出的错误,一共有 1800 + 条

  • scripts 目录:一些编译(或其他用途)脚本,如上述的 loc 目录中的国际化生成逻辑就在 scripts 目录中编写

  • src 目录:核心源码

ts-source-code-src-dirs.jpg

TS 编译流程

在互联网中鲜有对 TS 源码分析的文章,但是 TS 官方实际上有对 TS 原理的解释,如果你想了解更多可以阅读TypeScript 代码库内部的术语-Terminology_from inside_the_codebaseTypeScript 编译器内部结构-TypeScript_Compiler_Internals,和本文文末的参考文章

我们使用 TS 的目的就是要让他进行类型检查,通过后,输出为浏览器可直接解释执行的 JS 文件

TS 进行类型分析离不开语法树解析,而要将各种语法解析为一个树,就离不开词法分析

分析之前,你首先得认识这些语法

TS 对语法种类的定义,在 TypeScript/src/compiler/types.ts 文件中,搜索 export const enum SyntaxKind 即可找到

在 TS 中,每一种 Node,都有属性 kind,其值为上述的 SyntaxKind

export interface Node extends ReadonlyTextRange {
  readonly kind: SyntaxKind // 语法类型(包含 Token 类型,和 Node 类型)
  readonly flags: NodeFlags // 节点标记,对节点包含的一些特殊的东西的标记,如节点中含有 this,文件中含有 async 函数
  /* @internal */ modifierFlagsCache: ModifierFlags // 修饰符标记,如 public、private、protected、readonly 等
  /* @internal */ readonly transformFlags: TransformFlags // 节点包含需要转换语法(如 ES高级语法的转换)、特定转换(装饰器)等的标记
  readonly decorators?: NodeArray<Decorator> // 装饰器数组(按文档顺序)
  readonly modifiers?: ModifiersArray // 修饰符数组
  /* @internal */ id?: NodeId // 节点唯一的id
  readonly parent: Node // 父节点
  /* @internal */ original?: Node // 修改前的原始节点
  /* @internal */ symbol: Symbol // 符号(符号指声明的 Token 的 id) (绑定时初始化)
  /* @internal */ locals?: SymbolTable // 关联的局部变量(map 类型,符号表)
  /* @internal */ nextContainer?: Node // Next container in declaration order (绑定时初始化)
  /* @internal */ localSymbol?: Symbol // 节点声明的符号id (绑定时初始化,只针对导出的节点)
  /* @internal */ flowNode?: FlowNode // 控制流 节点 (绑定时初始化)
  /* @internal */ emitNode?: EmitNode // Associated EmitNode (initialized by transforms)
  /* @internal */ contextualType?: Type // 重载时用的临时上下文
  /* @internal */ inferenceContext?: InferenceContext // 推理上下文
}
复制代码

const a = 1 举例,它就包含了 SyntaxKind.VariableStatementSyntaxKind.VariableDeclarationListSyntaxKind.VariableDeclarationSyntaxKind.IdentifierSyntaxKind.NumberLiteral 五种

但是上面的 NodeSyntaxKind 都只是对语法的种类进行了(分)归类,TS 还是不知道 const 是什么含义,a 是什么含义,= 是什么含义, 1 是什么含义

TS 使用 unicode 定义了一组字符编码表 CharacterCodes(在 TypeScript/src/compiler/types.ts 文件中,搜索 export const enum CharacterCodes),这样就能判断 a 是一个 Identifier 了(变量名的起始字符规则和可以包含的字符规则是固定的,只需从上述字符编码表中进行匹配就可以确认是否为一个 Identifier),从而将 “代码字符串” 解析为一个个 SyntaxKind(Token)

export const enum CharacterCodes {
  // ...
  space = 0x0020, // " "
  _ = 0x5f,
  $ = 0x24,

  _0 = 0x30,
  _1 = 0x31,
  _2 = 0x32,
  _3 = 0x33,
  _4 = 0x34,
  _5 = 0x35,
  _6 = 0x36,
  _7 = 0x37,
  _8 = 0x38,
  _9 = 0x39,

  a = 0x61,
  b = 0x62,
  c = 0x63,
  d = 0x64,
  e = 0x65,
  // ...
}
复制代码

TS 中将 “代码字符串” 解析为 “SyntaxKind” 的行为叫 “扫描”,其使用 scan 函数进行处理(在 TypeScript/src/compiler/scanner.ts 文件中,搜索 function scan(): SyntaxKind {),其本质为一个 有限自动状态机

那么什么是状态机呢?它表示有限个状态以及在这些状态之间的转移和动作;比如在表格排序按钮中,一共有默认排序、升序、降序,那么在默认排序状态点击时,会变为升序状态;升序状态点击时,会变为降序状态;在降序状态点击时,会变为默认状态

在上述表头排序的例子中:

三种状态是有限的:默认、升、降 每次只能有一种状态 且能根据当前状态以一定的规则装换到下一个状态

回到代码中,以 = 字符举例,发现它,可能是 =>,可能是赋值符 =,可能是判断严格相等 === 的运算符(还有可能是 git 里冲突的标记符号)等

这在 scan 函数中是这样实现的

function scan(): SyntaxKind {
  while (true) {
    tokenFlags = TokenFlags.None
    const ch = codePointAt(text, pos)
    switch (ch) {
      case CharacterCodes.equals:
        // 如果是冲突标记
        if (isConflictMarkerTrivia(text, pos)) {
          pos = scanConflictMarkerTrivia(text, pos, error)
          if (skipTrivia) {
            continue
          } else {
            return (token = SyntaxKind.ConflictMarkerTrivia)
          }
        }
        // 如果下一个还是 =
        if (text.charCodeAt(pos + 1) === CharacterCodes.equals) {
          // 如果下一个又是 =
          if (text.charCodeAt(pos + 2) === CharacterCodes.equals) {
            return (pos += 3), (token = SyntaxKind.EqualsEqualsEqualsToken) // 那么它就是 ===
          }
          return (pos += 2), (token = SyntaxKind.EqualsEqualsToken) // 否则就是只有两个等号 ==
        }
        // 如果下一个是 >
        if (text.charCodeAt(pos + 1) === CharacterCodes.greaterThan) {
          return (pos += 2), (token = SyntaxKind.EqualsGreaterThanToken) // 那么就是 =>
        }
        pos++
        return (token = SyntaxKind.EqualsToken) // 上面都没匹配,就是赋值符 =
    }
  }
}
复制代码

有了上面 scan 得到的 Token 后,就可以 Token 的类别将源码解析成一个个 Node 并生成一个 AST 了(在 TypeScript/src/compiler/parser.ts 文件中,搜索 function parseSourceFileWorker(languageVersion: ScriptTarget 查看上述逻辑)

我们拿到的 Node 节点就和上面代码中的 interface Node 一样,包含节点的起始、结束位置信息、节点类型、引用关系等

在 Node 中可能存在引用外部变量的情况

如:

const a = 1

console.log(a)
复制代码

因此在 bind 阶段,会将三种情况的 Token 保存在符号表中

TypeScript/src/compiler/binder.ts 文件中的 function bind(node: Node | undefined): void 函数说明

其会将声明节点绑定到 symbol(符号) 规则有三种:

当前容器符号的“导出”表,如模块中的导出 当前容器符号的“成员”表,如对象中的属性 当前容器的“局部变量”表,如函数内的局部变量

如上述代码块,其根节点会保存符号表 locals,即局部变量 a

当上面所有的做完以后,ts 可以进行类型检查

以下面的代码块为例

const b: string = 2
复制代码

ts 会报错:不能将类型“number”分配给类型“string”

在 Node 节点中,还将保存类型信息,即 type 属性

解析后的 Node 节点为

{
  // ...
  "kind": 294,
  "statements": [
    {
      // ...
      "kind": 229,
      "declarationList": {
        // ...
        "kind": 247,
        "declarations": [
          {
            // ...
            "kind": 246,
            "name": {
              // ...
            },
            "initializer": {
              "pos": 17,
              "end": 19,
              "flags": 0,
              "modifierFlagsCache": 0,
              "transformFlags": 0,
              "parent": "[Circular ~.statements.0.declarationList.declarations.0]",
              "kind": 8,
              "text": "2",
              "numericLiteralFlags": 0
            },
            "type": {
              "pos": 8,
              "end": 15,
              "flags": 0,
              "modifierFlagsCache": 0,
              "transformFlags": 1,
              "parent": "[Circular ~.statements.0.declarationList.declarations.0]",
              "kind": 146
            }
          }
        ]
      }
    }
  ]
}
复制代码

在这种情况下,我们就可以通过 initializer,type 两个属性得出初始值和类型是否能通过类型检查了

当然 TS 进行类型检查的核心函数是 checkSourceElementWorker,其本质也是通过判断节点的 kind,然后执行不同的 check 函数

switch (kind) {
  case SyntaxKind.TypeParameter:
    return checkTypeParameter(node as TypeParameterDeclaration)
  case SyntaxKind.Parameter:
    return checkParameter(node as ParameterDeclaration)
  case SyntaxKind.PropertyDeclaration:
    return checkPropertyDeclaration(node as PropertyDeclaration)
  case SyntaxKind.PropertySignature:
    return checkPropertySignature(node as PropertySignature)
  // ...
}
复制代码

如校验元组中,每个成员必须都要具名都都不具名

function checkTupleType(node: TupleTypeNode) {
  const elementTypes = node.elements
  let seenOptionalElement = false
  let seenRestElement = false
  const hasNamedElement = some(elementTypes, isNamedTupleMember)
  for (const e of elementTypes) {
    if (e.kind !== SyntaxKind.NamedTupleMember && hasNamedElement) {
      grammarErrorOnNode(
        e,
        Diagnostics.Tuple_members_must_all_have_names_or_all_not_have_names
      )
      break
    }
  }
}
复制代码

对上文代码示例 const b: string = 2,会执行 reportRelationError 函数,将错误信息设置为 Diagnostics.Type_0_is_not_assignable_to_type_1

function reportRelationError(
  message: DiagnosticMessage | undefined,
  source: Type,
  target: Type
) {
  if (!message) {
    if (relation === comparableRelation) {
      message = Diagnostics.Type_0_is_not_comparable_to_type_1
    } else if (sourceType === targetType) {
      message =
        Diagnostics.Type_0_is_not_assignable_to_type_1_Two_different_types_with_this_name_exist_but_they_are_unrelated
    } else if (
      exactOptionalPropertyTypes &&
      getExactOptionalUnassignableProperties(source, target).length
    ) {
      message =
        Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties
    } else {
      if (
        source.flags & TypeFlags.StringLiteral &&
        target.flags & TypeFlags.Union
      ) {
        const suggestedType = getSuggestedTypeForNonexistentStringLiteralType(
          source as StringLiteralType,
          target as UnionType
        )
        if (suggestedType) {
          reportError(
            Diagnostics.Type_0_is_not_assignable_to_type_1_Did_you_mean_2,
            generalizedSourceType,
            targetType,
            typeToString(suggestedType)
          )
          return
        }
      }
      // 这里这里
      message = Diagnostics.Type_0_is_not_assignable_to_type_1
    }
  }
}
复制代码

因此,如果要快速找到 TS 中的某条报错信息对应的底层逻辑,可以到源码中搜索 Diagnostics 的属性

总结出 TS 的编译流程,我们可以回答上文的三个问题

TS 源代码经过词法扫描,生成Token标记,通过语法分析,生成语法树,再通过语义分析,生成符号表,进行类型检查等,最后将源代码编译为 JS 语法

TS 中的类型是什么?

在扫描阶段,TS 中的类型是一个个 Token,如 const a: string = '1' 中的 string

ts-string-keyword-example.jpg

执行 scan 函数时,扫描到 string 时,会认为它是一个 Identifier,然后调用 getIdentifierToken 函数,符合 textToKeyword 中的一个关键字 string ,标记为 150

ts-parser-string.jpg

根据解析到的 token,将 : string 解析为一个 TypeNode,如上图所示,并挂到最后生成的节点上

ts-node-type_node.jpg

type 属性其实是一个对象,因此,你可以理解为类型的本质是使用对象来描述的概念

TS 中的类型为什么不能当做值来使用?

我们知道,TS 最终通过会生成浏览器可执行的 JS 代码,在这期间会去掉类型的声明、使用和断言等,如果类型可以当值使用了,那么就会出现这样的情况

interface A {}
const a = A
const b = 1
复制代码

TS 编译后,会变成

const a =
const b = 1
复制代码

这显然不符合 JS 语法规范,1 是 const 声明的常量必须赋初始值,2 是赋值符右边不能没有东西

TS 中是如何处理像“类型不能当一个值来使用”的错误的?

根据上文的分析,在 bind 阶段,会将三种东西放到符号表中

当前容器符号的“导出”表,如模块中的导出
当前容器符号的“成员”表,如对象中的属性
当前容器的“局部变量”表,如函数内的局部变量
复制代码

符号表中还包含了我们声明的类型

假设类型做为值时,根据 JS 读取变量的规则,会到作用域链上依次查找,也就是每个作用域下的局部(全局)变量,如果找到的是一个类型,自然就能抛出错误

再根据上文提到的各种 Diagnostics,得到报错的信息

把类型打印出来

我们想得到的类型长什么样呢?把上文提到的 TypeNode 打印出来吗?那应该没有很多人看的懂吧

我们能否把类型转换为字符串呢?答案是可以的

TS compiler API 提供了把 TS 文件,转换为 JS 文件的方法,参考文档,一个简单的变换函数_ts.transpileModule

ts.transpileModule 中,可以添加 transformers,即自定义的转换器

ts.transpileModule(yourFileData /* 你读取的 ts 文件的结果 */, {
  transformers: { before: [xxx /* 自定义的转换器 */] },
})
复制代码

在自定义转换器中,我们可以把能找到类型声明的标识符替换一下,得到其对应的类型字符串

首先创建两个文件

type-print.mjstest.ts

要提取类型,并将其转换为类型字符串,那么需要调用 ts.createProgram 生成一个 program,然后调用 program.getTypeChecker 获得 typeChecker

type-print.mjs

import ts from "typescript"
import fs from "fs"

const program = ts.createProgram(["./test.ts"], { emitDeclarationOnly: false })
const result = ts.transpileModule(fs.readFileSync("./test.ts").toString(), {
  transformers: { before: [createTransformer(program.getTypeChecker())] },
})
复制代码

接下来实现 createTransformer 函数

/**
 * 创建一个 transformer
 * @param {ts.TypeChecker} typeChecker
 */
const createTransformer = (typeChecker) => {
  return (context) => {
    return (node) => ts.visitNode(node, visit /* 我们需要实现这个函数 */)
  }
}
复制代码

在上述函数中,transform 会遍历所有节点,然后可以对节点进行一些替换操作,我们需要实现 visit 函数

/**
 * node 遍历
 * @param {ts.Node} node
 */
function visit(node) {
  // 如果 node 的类型 (SyntaxKind) 是 标识符 (Identifier),并且有 id,那么就认为是把类型当做值来使用
  // 找到节点的父节点,并依次向父级查找,依次查找,直到找到含有 locals 属性且该映射表中含有当前标识符的节点
  // 用 typeChecker.getTypeAtLocation 得到 type
  // 然后在不同的 type 中,执行不同的生成类型字符串的逻辑
  // 其最生成逻辑是调用 typeChecker.typeToString,并将结果传递给 ts.factory.createStringLiteral 函数,将节点替换为 StringLiteral
}
复制代码

visit 函数的逻辑我们梳理了一遍,接下来就是实现部分了

我们首先假设我们要打印的类型很简单

test.ts

type A = 1
console.log(A)
复制代码

在 ts 的 Type 类型中,有 flags 用于标识类型的种类,如这里的 A 就是数字字面量类型,那么其种类就为 ts.TypeFlag.NumberLiteral

当然,在判断类型之前,首先要拿到类型

if (node.kind === ts.SyntaxKind.Identifier && node.id) {
  // 父节点
  let parent = node.parent
  // 类型的符号
  let symbol
  // 符号标记,即能不能找到对应的声明
  let symbolFlag
  // 要被打印的类型的字符串初始值,未知类型打印 error
  let typeStr = "error"

  const updateTypeStr = (str) => {
    if (typeStr === "error") typeStr = str
  }
  /*
   * 查找父节点的 locals,拿到类型的符号
   */
  while (
    !(
      parent &&
      parent.locals instanceof Map &&
      parent.locals.size > 0 &&
      symbolFlag
    ) &&
    parent
  ) {
    const locals = parent.locals
    symbolFlag = !!parent.locals && (symbol = locals.get(node.escapedText))
    parent = parent.parent
  }
  if (!symbolFlag) return ts.visitEachChild(node, visit, context)
  // 通过 typeChecker.getTypeAtLocation 函数,这里的 type 就是我们拿到的类型
  const type = typeChecker.getTypeAtLocation(symbol.declarations[0].name)
}
复制代码

通过 type.flags 即可判断类型的种类,由于 ts 内部是通过二进制(表现仍为10进制)来存储类型种类的,因此需要用逻辑与 & 来判断是否包含此类型

if (type.flags & ts.TypeFlag.NumberLiteral) {
    updateTypeStr(typeChecker.typeToString(type))
}
return ts.factory.createStringLiteral(typeStr)
复制代码

这样就可以得到结果 result

最后生成文件并执行,即可打印出 1

fs.writeFileSync("./index.js", result.outputText)
复制代码

生成文件结果

index.js

console.log("2");
复制代码
node ./index.js
# 1
复制代码

当然除了数字是字面量类型,还有很多其他字面量类型,这里可以直接用 ts.TypeFlag.Literal 来判断

除了简单类型,还有如 interface(接口),联合类型 (string | number)、相交类型({ a: 1 } & { b: 2 })之类的复杂类型

我们都可以像上面一样写判断一一处理

完整代码如下:

type-print.mjs

import ts from "typescript"
import fs from "fs"
import path from "path"

/**
 *
 * @param {ts.TypeChecker} typeChecker
 * @returns
 */
const createTransformer = (typeChecker) => {
  return (context) => {
    return (node) => ts.visitNode(node, visit)
    /**
     *
     * @param {ts.Node} node
     * @returns
     */
    function visit(node) {
      if (node.kind === ts.SyntaxKind.Identifier && node.id) {
        let parent = node.parent
        let symbol
        let symbolFlag
        let typeStr = "error"

        const updateTypeStr = (str) => {
          if (typeStr === "error") typeStr = str
        }

        while (
          !(
            parent &&
            parent.locals instanceof Map &&
            parent.locals.size > 0 &&
            symbolFlag
          ) &&
          parent
        ) {
          const locals = parent.locals
          symbolFlag =
            !!parent.locals && (symbol = locals.get(node.escapedText))
          parent = parent.parent
        }

        if (!symbolFlag) return ts.visitEachChild(node, visit, context)
        
        const type = typeChecker.getTypeAtLocation(symbol.declarations[0].name)
        
        if (type.flags & ts.TypeFlags.Object) {
          if (
            type.flags & ts.TypeFlags.Enum ||
            type.flags & ts.TypeFlags.EnumLike ||
            type.flags & ts.TypeFlags.EnumLiteral
          ) {
            return ts.factory.createIdentifier(typeChecker.typeToString(type))
          }
          
          const fields = []
          const properties = type.getProperties()

          for (const prop of properties) {
            const name = prop.getName()
            const propType = typeChecker.getTypeOfSymbolAtLocation(prop, node)
            const propTypeName = typeChecker.typeToString(propType)
            const hasQuestionToken = prop.valueDeclaration ? !!prop.valueDeclaration.questionToken : false
            fields.push(
              `${name}${hasQuestionToken ? "?" : ""}: ${propTypeName};`
            )
          }

          updateTypeStr(fields.length > 0 ? `{\n\t${fields.join("\n   ")}\n}` : '{}')
        } else if (type.flags & ts.TypeFlags.UnionOrIntersection) {
            const fields = []

            for (const prop of type.types) {
                fields.push(typeChecker.typeToString(prop))
            }

            let separator = ''
            if (type.flags & ts.TypeFlags.Union) {
                separator = '|'
            } else if (type.flags & ts.TypeFlags.Intersection) {
                separator = '&'
            }

            updateTypeStr(fields.join(` ${separator} `))
        } else if (type.flags & ts.TypeFlags.Literal) {
            updateTypeStr(typeChecker.typeToString(type))
        }

        return ts.factory.createStringLiteral(typeStr)
      }
      // 其它节点保持不变
      return ts.visitEachChild(node, visit, context)
    }
  }
}

function resolvePath(inputPath) {
    return path.resolve(process.cwd(), inputPath)
}

const program = ts.createProgram([resolvePath('./test.ts')], { emitDeclarationOnly: false })
const result = ts.transpileModule(fs.readFileSync(resolvePath('./test.ts')).toString(), {
  transformers: { before: [createTransformer(program.getTypeChecker())] },
})

fs.writeFileSync("./index.js", result.outputText)

console.log("编译成功")

复制代码

最后结果

ts-print-demo.jpg

你可以在 github 中获取这段代码 github-ts-type-print,本文原文请戳这里

参考文章(力荐):

博客园-TypeScript源码详细解读(1)总览

博客园-TypeScript源码详细解读(2)词法1-字符处理

博客园-TypeScript源码详细解读(3)词法2-标记解析

博客园-TypeScript 源码详细解读(4)语法1-语法树

博客园-TS原理详细解读(5)语法2-语法解析

博客园-TS原理详细解读(7)绑定1-符号

博客园-给萌新的TS_custom_transformer_plugin教程——TypeScript 自定义转换器插件

StackOverflow-print-typescript-types-into-console-file

CSDN-浅析TypeScript_Compiler原理

我读 Typescript 源码的秘诀都在这里了

猜你喜欢

转载自juejin.im/post/7108496585071263781