This article Outlines how I read Typescript source code, and similar techniques can be used to read source code for other libraries.
Let’s start with a ts syntax:
The advanced type Test<T>, which takes a generic parameter T, has two cases when T is passed as a union type:
-
If checkType (the type to the left of extends) is T, the union type is broken down, parsed, and finally merged into a union type.
-
If the checkType is not T, parse the union type as a whole as T and return the resolved type.
This syntax is called Distributive Condition Type. The purpose of design is to simplify the Test < number > | Test < Boolean >.
This syntax design is not discussed here, we use the implementation of this syntax as a starting point to explore how to read TS source code.
Representation of type: type object
Ts parses the source code, generates an AST, and parses the type information from the AST.
Ts type information is stored through type objects. Let’s look at a few examples. (A visual view of the AST can be viewed using astExplorer.net.)
Four types are defined above:
LiteralType is LiteralType, literal property holds the literal, in this case a NumericLiteral.
The b type is UnionType, the UnionType, and the types attribute holds the type it contains, in this case two literaltypes
T extends Boolean This part is a ConditionType with checkType, extendsType, trueType, and falseType attributes representing different parts.
As you can see, T is a TypeReference type, that is, it is just a variable reference, and the specific value is the type passed in by the generic parameter.
The Test < number | Boolean > is also a TypeReference, reference types. There are two properties: typeName, which refers to the type Test, and typeArguments, which refers to the value of the generic parameter, UnionType.
So, types in TS are represented by type objects.
The TypeReference type is special. It is just a reference, and a specific type has to pass the type argument to the referenced type, and then figure out the final type. Such as the Test here < number | Boolean > type, the type is the number of the parameter | Boolean to define the ConditionType to calculate. This is the advanced type of TS.
Understand the type of what is said, what are high-level types and generic parameters, then we can formally adopted debugging ts source see ConditionType parsing process.
VSCode debugs Typescript source code
First, we need to download the ts source code.
git clone --depth=1 [email protected]:microsoft/TypeScript.git
Copy the code
You can then see that the lib directory has tsc.js and typescript.js, which are the command line and API entry points for TS, respectively.
However, these are compiled JS code, the source code under SRC, is written in TS.
How to associate the compiled JS code with ts source code? Sourcemap!
The default compiler does not have sourcemap, so we need to modify the compiler configuration:
Modify SRC /tsconfig-library-base.json (this is the compiler configuration for ts to generate lib code) to change sourceMap to true.
Then compile the source code:
yarn
yarn run build:compiler
Copy the code
You can then see a built directory with entry files tsc.js and typescript. Js, and sourcemap as well:
Now you can debug the TS source code directly instead of the compiled JS code. The letter?
Let’s try it.
Vscode directly debugs ts
Vscode saves the debug configuration under.vscode/launch.json in the project root directory:
Let’s add a debug configuration:
{
"name": "Debug TS source code"."program": "${workspaceFolder}/built/local/tsc.js"."request": "launch"."skipFiles": [
"<node_internals>/**"]."args": [
"./input.ts"]."stopOnEntry": true."type": "node"
}
Copy the code
The meanings are as follows:
- Name: indicates the debugging configuration name
- Program: Address of the target program for debugging
- Request: There are two values: launch and attch, indicating whether to launch a new one or connect to an existing one
- SkipFiles: Skip some files during debugging. Skip those files inside node to make the call stack cleaner
- Args: command line parameter
- StopOnEntry: Whether to add a breakpoint on the first line
- Type: debug type, in this case run with Node
After saving, you can see the debug option in the debug panel:
Here we design input.ts like this:
type Test<T> = T extends boolean ? "Y" : "N";
type res = Test<number | boolean>;
Copy the code
Make a breakpoint in the checker.ts section of ts and click Start debugging.
Then, look, the broken place, is ts source ah, not compiled JS file. That’s where Sourcemap comes in.
You can also see the directory structure of the source code in the left file tree, which is much better than debugging compiled JS code.
Now that we know how to debug the source code through Sourcemap, it’s time to move on to the subject: exploring the implementation of distributed conditional types through source code.
In fact, we use the command line entry of tsc.js to debug, so that there are many codes, it is difficult to clarify which part of the code to see. What to do?
Now comes my secret weapon, using the typescript Compiler API.
typescript compiler api
Ts provides an API in addition to a command-line tool entry, but we rarely use it. But it is very helpful to explore the TS source implementation.
We define a test.js file and import the typescript package:
const ts = require("./built/local/typescript");
Copy the code
Then use the TS API passed in to compile the configuration, and parse the source code into ast:
const filename = "./input.ts";
const program = ts.createProgram([filename], {
allowJs: false
});
const sourceFile = program.getSourceFile(filename);
Copy the code
The second parameter to createProgram is the build configuration, so I passed it allowJS.
Program. GetSourceFile returns the AST of the TS.
And you can also get typeChecker:
const typeChecker = program.getTypeChecker();
Copy the code
And then what? TypeChecker is the type checking API. We can traverse the AST to find the node to check, and then call the Checker API to check:
function visitNode(node) {
if (node.kind === ts.SyntaxKind.TypeReference) {
const type = typeChecker.getTypeFromTypeNode(node);
debugger;
}
node.forEachChild(child= >
visitNode(child)
);
}
visitNode(sourceFile);
Copy the code
We judge if the AST is TypeReference type, use typeChecker. GetTypeFromTypeNode to parse the type.
You can then debug the logic parsed by the type precisely, which is much easier to untangle than the command line approach.
The complete code is as follows:
const ts = require("./built/local/typescript");
const filename = "./input.ts";
const program = ts.createProgram([filename], {
allowJs: false
});
const sourceFile = program.getSourceFile(filename);
const typeChecker = program.getTypeChecker();
function visitNode(node) {
if (node.kind === ts.SyntaxKind.TypeReference) {
const type = typeChecker.getTypeFromTypeNode(node);
debugger;
}
node.forEachChild(child= >
visitNode(child)
);
}
visitNode(sourceFile);
Copy the code
Let’s change the debug configuration and start debugging:
{
"name": "Debug TS source code"."program": "${workspaceFolder}/test.js"."request": "launch"."skipFiles": [
"<node_internals>/**"]."args": []."type": "node"
}
Copy the code
In typeChecker getTypeFromTypeNode this line a breakpoint, shall we go to the parsing process under the specific type.
Then, XDM, brace yourself for the climax of this article:
We get into the getTypeFromTypeNode method, which does different parsing based on the AST type and returns type objects. This is where the logic for all types of parsing comes in, and it’s an important transportation hub.
Then we entered the TypeReference branch, because the Test < number | Boolean > is a reference type.
The type of TypeReference is the type it refers to. This extends Boolean extends ConditionType.
All types are cached in a nodeLinks map according to the AST node ID, and only need to be parsed for the first time and then retrieved directly. For example, the resolvedType above is cached with nodeLinks.
And then, XDM, see that shiny line of code?
The isDistributive attribute is set based on whether the checkType part of ConditionType is a TypeParameter.
When we parse the TypeReference type, we pass in the concrete type to instantiate it:
ConditionType is the isDistributive property of conditionType. If it is, each type of unionType is passed in for parsing and then merged back.
In the figure, we go to the branch where isDistributive is true.
Then analyze the type is’ Y ‘|’ N ‘type of joint.
Let’s change the input.ts code:
type Test<T> = [T] extends [boolean]?"Y" : "N";
type res = Test<number | boolean>;
Copy the code
CheckType does not write the type parameter T directly.
One more run:
Not this time.
Or that?
Indeed, this is N.
What does it show? ConditionType (isDistributive) ConditionType (checkType) ConditionType (isDistributive) ConditionType (TypeReference)
So as long as the checkType is not T.
So this works:
This also works:
We often use [T] distributive to avoid distributive, but this is more concise.
In this way, we have clarified the implementation of this syntax through the source code.
conclusion
We read the typescript source code to explore how distributive Condition Type is implemented.
Download the typescript source code, change the compiler configuration to generate code with sourcemap, and then debug it in vscode to directly debug the compiled source code.
Typescript has cli and API entrances. Cli is too much irrelevant code to make sense of, so we use API to write a test code, and then break points to debug.
The ts type information is stored in the type object, which can be visually viewed using astExplorer.net.
Use typeChecker. GetTypeFromTypeNode can get a certain types of specific values, we are through the parsed as entrance to explore various types of logic.
There are several important points in the source code:
- The getTypeFromTypeNode method is the entry method to get the type from Node. All AST type objects are retrieved from this method
- NodeLinks saves the parse type, and the key is the node ID.
ConditionType is set to isDistributive depending on whether checkType is a type parameter. TypeReference then instantiates the type into different processing logic depending on the value of isDistributive, which is how it works.
Once we understand the principle, we can then use distributive Condition Type with a foundation in mind, and we can create many transformations that are not limited to [T].
This article in order to debug a type of logic for the principle of exploring ts source code reading way, debugging TS other parts of the code, or debugging other libraries are similar.
This will help you learn how to debug typescript source code so that when you want to explore the implementation of a type syntax, you can thoroughly understand the source code level. Before the source code, there is no secret.