这篇文章梳理了我阅读Typescript源码的方式,类似的技巧也可以用于阅读其他库的源码。以一个ts语法开头:Test,一个高级类型,有一个泛型参数T。当T传入的类型是联合类型时,有两种情况:如果checkType(extends左边的类型)是T,关节类型被拆解,然后类型被解析,最后合并为一个关节类型返回。如果checkType不是T,则将联合类型作为一个整体解析为T,并返回解析后的类型。这种语法叫做DistributiveConditionType,分布式条件类型。设计的目的是为了简化Test|的情况测试<布尔值>。这里先不说语法是怎么设计的。下面我们就以这个语法的实现为出发点,来探讨一下ts源码应该怎么读。类型表示:类型对象ts会解析源代码,生成AST,然后从AST中解析出类型信息。ts的类型信息是通过类型对象来存储的,我们来看几个例子。(可以通过astexplorer.net网站直观查看AST。)上面定义了四种类型:类型a是LiteralType,字面量类型,literal属性存储具体的字面量。这是NumericLiteral,一个数字文字。b类型是UnionType,一个联合类型,types属性保存了它包含的类型。这里有两个LiteralTypeTextendsboolean。这部分是一个ConditionType。有四个属性:checkType、extendsType、trueType、falseType,分别代表不同的部分。可以看出T是一个TypeReference类型,即它只是一个变量引用,具体取值是泛型参数传入的类型。测试<数字|boolean>也是一个TypeReference,一个类型引用。有两个属性,typeName和typeArguments,typeName是它引用的类型Test,typeArguments是泛型参数的值,这里是UnionType。因此,类型在ts中由类型对象表示。比较特殊的是TypeReference类型,它只是一个引用。对于特定类型,必须将类型参数传递给引用类型,然后计算最终类型。例如,对于类型Test这里,通过传递参数number|得到最终的类型布尔值到定义的ConditionType中。这是ts的高级类型。了解了类型是如何表示的,有哪些高级类型和泛型参数,那么我们就可以正式调试ts源码,看看ConditionType的解析过程。VSCode调试Typescript源码首先我们需要下载ts源码(加一个depth=1下载单个commit,速度更快)gitclone--depth=1git@github.com:microsoft/TypeScript.git然后可以看到lib目录下有tsc.js和typescript.js,分别是ts的命令行和api的入口。不过这些都是编译好的js代码,源码在src下,用ts写的。如何将编译后的js代码与ts源码关联起来?来源地图!默认编译的产品没有sourcemap。我们需要修改编译配置:修改src/tsconfig-library-base.json,(这是ts生成lib代码的编译配置),将sourceMap改为true。然后编译源码:yarnyarnrunbuild:compiler然后可以看到多了一个build目录,下面有两个入口文件tsc.js和typescript.js,还有一个sourcemap:接下来就可以直接调试ts了sourcecode而不是编译后的js代码就上来了。你相信吗?不信我们去试试。vscode直接调试tsvscode将调试配置保存在项目根目录.vscode/launch.json下:我们添加一个调试配置:{"name":"debuggingtssourcecode","program":"${workspaceFolder}/built/local/tsc.js","re??quest":"launch","skipFiles":["/**"],"args":["./input.ts"],"stopOnEntry":true,"type":"node"}含义如下:name:调试配置程序的名称:要调试的目标程序的地址request:有launch和attch两个值,代表是否启动aneworconnecttotheexistingskipFiles:用于调试时跳过一些文件,这里的配置是跳过节点内部的那些文件,调用栈会更简洁。保存后在调试面板可以看到调试选项:这里我们把input.ts设计成这样:typeTest=Textendsboolean?"Y":"N";typeres=Test;在ts的checker.ts部分打断点,点击StartDebugging。然后看,它停的地方是ts源码,不是编译好的js文件。这就是sourcemaps所做的。还可以在左侧的文件树中看到源码的目录结构,比调试编译好的js代码爽多了。遇到通过sourcemap调试源码后,就该进入正题了:通过源码探索分布式条件类型的实现原理。其实上面我们是使用tsc.js的命令行入口调试的,所以代码其实很多,很难理清到底看哪部分代码。怎么做?接下来是我的秘密武器,使用typescriptcompilerapi。除了命令行工具的入口,typescriptcompilerapits也提供了api的形式,只是我们很少用到。但是对于探索ts源码实现很有帮助。我们定义一个test.js文件并导入typescript包:constts=require("./built/local/typescript");然后使用tsAPI传入编译配置,将源码解析成ast:constfilename="./input.ts";constprogram=ts.createProgram([filename],{allowJs:false});constsourceFile=program.getSourceFile(文件名);这里createProgram的第二个参数是编译配置,我传入了一个allowJS的意思。.program.getSourceFile返回ts的AST。您还可以获得类型检查器:consttypeChecker=program.getTypeChecker();然后呢?typeChecker是一个用于类型检查的api。我们可以遍历AST找到要检查的节点,然后调用检查器api进行检查:调试器;}node.forEachChild(child=>visitNode(child));}visitNode(sourceFile);我们判断如果AST是TypeReference类型,则使用typeChecker.getTypeFromTypeNode解析类型。接下来就可以准确调试这类解析的逻辑了。与命令行方式相比,更容易理清逻辑。完整代码如下:constts=require("./built/local/typescript");constfilename="./input.ts";constprogram=ts.createProgram([filename],{allowJs:false});constsourceFile=程序。getSourceFile(文件名);consttypeChecker=program.getTypeChecker();functionvisitNode(node){if(node.kind===ts.SyntaxKind.TypeReference){consttype=typeChecker.getTypeFromTypeNode(node);调试器;}node.forEachChild(child=>visitNode(child));}visitNode(sourceFile);我们更改调试配置,然后开始调试:{"name":"debugtssourcecode","program":"${workspaceFolder}/test.js","request":"launch","skipFiles":["/**"],"args":[],"type":"node"}在typeChecker.getTypeFromTypeNode行中放置一个断点,我们看具体的类型解析过程。然后,XDM,打起精神,本文的高潮来了:我们进入了getTypeFromTypeNode方法,这个方法是根据AST的类型做不同的解析,返回类型对象。各种类型解析的逻辑都是从这里进入的,是一个重要的交通枢纽。然后我们进入TypeReference分支,因为Test只是一个类型引用。TypeReference的类型就是它引用的类型,它引用的是ConditionType,所以会分析ConditionType的类型Textendsboolean:所有的类型都按照ast节点的id存储在一个nodeLinks的map中去缓存,只第一次需要解析,然后直接得到结果。比如上图中的resolvedType存放在nodeLinks中进行缓存。而且,XDM,看到那行闪亮的代码了??吗?在解析ConditionType的类型时,会根据checkType部分是否为类型参数(TypeParameter,即泛型)来设置isDistributive属性。后面解析TypeReference类型时,会传入具体的类型进行实例化:这里判断conditionType的isDistributive属性,如果是,则分别传入各类型的unionType进行解析,最后合并返回。如图所示,我们已经到了isDistributive为true的分支。那么解析出来的类型就是'Y'的联合类型|'N'。那我们改下input.ts的代码:typeTest=[T]extends[boolean]?"Y":"N";typeres=Test;checkType不直接写类型参数T。再次运行:这次没有进去。难道是那个?确实,这样的结果是N,什么意思呢?说明ConditionType根据checkType是否为类型参数设置isDistributive属性,然后根据isDistributive的值分别解析TypeReference。然后只要checkType不是T就可以了。所以这很好:这也很好:我们经常使用[T]来避免分配,但这更简洁。看了源码我们知道,其实其他方法也可以。这样,我们就通过源码明确了这个语法的实现原理。总结我们阅读typescript源码的目的是探索分布式条件类型的实现原理。先下载typescript源码,然后改编译配置为用sourcemap生成代码,然后在vscode中调试,这样就可以在编译前直接调试源码了,信息比较多。Typescript有两个入口,cli和api。cli的方式不相关的代码太多,很难梳理,所以我们用api的方式写了一段测试代码,然后断点调试。ts的类型信息保存在type对象中,可以用astexplorer.net直观查看。使用typeChecker.getTypeFromTypeNode获取某个类型的具体值。我们以此为切入点,探索各种类型的分析逻辑。源码中比较重要的几点如下:getTypeFromTypeNode方法是通过node获取类型的入口方法。所有的AST类型对象都是通过这个方法获取的。NodeLinks保存的是解析出来的type,key是nodeid。嗯,下次取缓存。然后我们看到ConditionType的解析逻辑会根据checkType是否为类型参数设置isDistributive属性,然后TypeReference在实例化这个类型时,会根据isDistributive的值进入不同的处理逻辑,这就是它的实现原理。了解原理后,我们就可以放心使用分布式条件类型了,我们可以创造出很多变体,不局限于[T]。本文以调试一个类型解析逻辑的原理为出发点,探索阅读ts源码之道。调试ts代码其他部分或者调试其他库类似。希望能帮助大家掌握typescript源码的调试技巧。当你想探究某类语法的实现原理时,可以通过源码层面深入了解。在源代码面前,没有秘密。