探索類型系統(tǒng)的底層 - 自己實(shí)現(xiàn)一個(gè) TypeScript(硬核干貨)
原文:https://indepth.dev/under-the-hood-of-type-systems/
DawnL 譯
這篇文章包含兩個(gè)部分:
A 部分:類型系統(tǒng)編譯器概述(包括 TypeScript)
Syntax vs Semantics 語(yǔ)法 vs 語(yǔ)義 What is AST? 什么是 AST? Types of compilers 編譯器的類型 What does a language compiler do? 語(yǔ)言編譯器是做什么的? How does a language compiler work? 語(yǔ)言編譯器是如何工作的? Type system compiler jobs 類型系統(tǒng)編譯器職責(zé) Advanced type checker features 高級(jí)類型檢查器的功能
B 部分:構(gòu)建我們自己的類型系統(tǒng)編譯器
The parser 解析器 The checker 檢查器 Running our compiler 運(yùn)行我們的編譯器 What have we missed? 我們遺漏了什么?
A 部分:類型系統(tǒng)編譯器概述
語(yǔ)法 vs 語(yǔ)義
語(yǔ)法和語(yǔ)義之間的區(qū)別對(duì)于早期的運(yùn)行很重要。
語(yǔ)法 - Syntax
語(yǔ)法通常是指 JavaScript 本機(jī)代碼。本質(zhì)上是詢問(wèn)給定的 JavaScript 代碼在運(yùn)行時(shí)是否正確。
例如,下面的語(yǔ)法是正確的:
var foo: number = "not a number";
語(yǔ)義 - Semantics
這是特定于類型系統(tǒng)的代碼。本質(zhì)上是詢問(wèn)附加到代碼中的給定類型是否正確。
例如,上面的代碼在語(yǔ)法上是正確的,但在語(yǔ)義上是錯(cuò)誤的(將變量定義為一個(gè)數(shù)字類型,但是值是一個(gè)字符串)。
接下來(lái)是 JavaScript 生態(tài)系統(tǒng)中的 AST 和編譯器。
什么是 AST?
在進(jìn)一步討論之前,我們需要快速了解一下 JavaScript 編譯器中的一個(gè)重要機(jī)制 AST。
關(guān)于 AST 詳細(xì)介紹請(qǐng)看這篇文章。
AST 的意思是抽象語(yǔ)法樹(shù) ,它是一個(gè)表示程序代碼的節(jié)點(diǎn)樹(shù)。Node 是最小單元,基本上是一個(gè)具有 type 和 location 屬性的 POJO(即普通 JavaScript 對(duì)象)。所有節(jié)點(diǎn)都有這兩個(gè)屬性,但根據(jù)類型,它們也可以具有其他各種屬性。
在 AST 格式中,代碼非常容易操作,因此可以執(zhí)行添加、刪除甚至替換等操作。
例如下面這段代碼:
function add(number) {
return number + 1;
}
將解析成以下 AST:

編譯器類型
在 JavaScript 生態(tài)系統(tǒng)中有兩種主要的編譯器類型:
1. 原生編譯器(Native compiler)
原生編譯器將代碼轉(zhuǎn)換為可由服務(wù)器或計(jì)算機(jī)運(yùn)行的代碼格式(即機(jī)器代碼)。類似于 Java 生態(tài)系統(tǒng)中的編譯器 - 將代碼轉(zhuǎn)換為字節(jié)碼,然后將字節(jié)碼轉(zhuǎn)換為本機(jī)代碼。
2. 語(yǔ)言編譯器
語(yǔ)言編譯器扮演著不同的角色。TypeScript 和 Flow 的編譯器在將代碼輸出到 JavaScript 時(shí)都算作語(yǔ)言編譯器。
語(yǔ)言編譯器與原生編譯器的主要區(qū)別在于,前者的編譯目的是 tooling-sake(例如優(yōu)化代碼性能或添加附加功能),而不是為了生成機(jī)器代碼。
語(yǔ)言編譯器是做什么的?
在類型系統(tǒng)編譯器中,總結(jié)的兩個(gè)最基本的核心職責(zé)是:
1. 執(zhí)行類型檢查
引入類型(通常是通過(guò)顯式注解或隱式推理),以及檢查一種類型是否匹配另一種類型的方法,例如 string 和 number。
2. 運(yùn)行語(yǔ)言服務(wù)器
對(duì)于一個(gè)在開(kāi)發(fā)環(huán)境中工作的類型系統(tǒng)(type system)來(lái)說(shuō),最好能在 IDE 中運(yùn)行任何類型檢查,并為用戶提供即時(shí)反饋。
語(yǔ)言服務(wù)器將類型系統(tǒng)連接到 IDE,它們可以在后臺(tái)運(yùn)行編譯器,并在用戶保存文件時(shí)重新運(yùn)行。流行的語(yǔ)言,如 TypeScript 和 Flow 都包含一個(gè)語(yǔ)言服務(wù)器。
3. 代碼轉(zhuǎn)換
許多類型系統(tǒng)包含原生 JavaScript 不支持的代碼(例如不支持類型注解) ,因此它們必須將不受支持的 JavaScript 轉(zhuǎn)換為受支持的 JavaScript 代碼。
關(guān)于代碼轉(zhuǎn)換更詳細(xì)的介紹,可以參考原作者的這兩篇文章 Web Bundler 和 Source Maps。
語(yǔ)言編譯器是如何工作的?
對(duì)于大多數(shù)編譯器來(lái)說(shuō),在某種形式上有三個(gè)共同的階段。
1. 將源代碼解析為 AST
詞法分析 -> 將代碼字符串轉(zhuǎn)換為令牌流(即數(shù)組) 語(yǔ)法分析 -> 將令牌流轉(zhuǎn)換為 AST 表示形式
解析器檢查給定代碼的語(yǔ)法。類型系統(tǒng)必須有自己的解析器,通常包含數(shù)千行代碼。
Babel 解析器 中的 2200+ 行代碼,僅用于處理 statement 語(yǔ)句(請(qǐng)參閱此處)。
Hegel 解析器將 typeAnnotation 屬性設(shè)置為具有類型注解的代碼(可以在這里看到)。
TypeScript 的解析器擁有 8900+ 行代碼(這里是它開(kāi)始遍歷樹(shù)的地方)。它包含了一個(gè)完整的 JavaScript 超集,所有這些都需要解析器來(lái)理解。
2. 在 AST 上轉(zhuǎn)換節(jié)點(diǎn)
操作 AST 節(jié)點(diǎn)
這里將執(zhí)行應(yīng)用于 AST 的任何轉(zhuǎn)換。
3. 生成源代碼
將 AST 轉(zhuǎn)換為 JavaScript 源代碼字符串
類型系統(tǒng)必須將任何非 js 兼容的 AST 映射回原生 JavaScript。
類型系統(tǒng)如何處理這種情況呢?
類型系統(tǒng)編譯器(compiler)職責(zé)
除了上述步驟之外,類型系統(tǒng)編譯器通常還會(huì)在解析之后包括一個(gè)或兩個(gè)額外步驟,其中包括特定于類型的工作。
順便說(shuō)一下,TypeScript 的編譯器實(shí)際上有 5 個(gè)階段,它們是:
語(yǔ)言服務(wù)預(yù)處理器 - Language server pre-processor 解析器 - Parser 結(jié)合器 - Binder 檢查器 - Checker 發(fā)射器 - Emitter
正如上面看到的,語(yǔ)言服務(wù)器包含一個(gè)預(yù)處理器,它觸發(fā)類型編譯器只在已更改的文件上運(yùn)行。這會(huì)監(jiān)聽(tīng)任意的 import 語(yǔ)句,來(lái)確定還有哪些內(nèi)容可能發(fā)生了更改,并且需要在下次重新運(yùn)行時(shí)攜帶這些內(nèi)容。
此外,編譯器只能重新處理 AST 結(jié)構(gòu)中已更改的分支。關(guān)于更多 lazy compilation,請(qǐng)參閱下文。
類型系統(tǒng)編譯器有兩個(gè)常見(jiàn)的職責(zé):
1. 推導(dǎo) - Inferring
對(duì)于沒(méi)有注解的代碼需要進(jìn)行推斷。關(guān)于這點(diǎn),這里推薦一篇關(guān)于何時(shí)使用類型注解和何時(shí)讓引擎使用推斷的文章。
使用預(yù)定義的算法,引擎將計(jì)算給定變量或者函數(shù)的類型。
TypeScript 在其 Binding 階段(兩次語(yǔ)義傳遞中的第一次)中使用最佳公共類型算法。它考慮每個(gè)候選類型并選擇與所有其他候選類型兼容的類型。上下文類型在這里起作用,也會(huì)做為最佳通用類型的候選類型。在這里的 TypeScript 規(guī)范中有更多的幫助。
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
TypeScript 實(shí)際上引入了 Symbols(interface)的概念,這些命名聲明將 AST 中的聲明節(jié)點(diǎn)與其他聲明進(jìn)行連接,從而形成相同的實(shí)體。它們是 TypeScript 語(yǔ)義系統(tǒng)的基本構(gòu)成。
2. 檢查 - Checking
現(xiàn)在類型推斷已經(jīng)完成,類型已經(jīng)分配,引擎可以運(yùn)行它的類型檢查。他們檢查給定代碼的 semantics。這些類型的檢查有很多種,從類型錯(cuò)誤匹配到類型不存在。
對(duì)于 TypeScript 來(lái)說(shuō),這是 Checker (第二個(gè)語(yǔ)義傳遞) ,它有 20000+ 行代碼。
我覺(jué)得這給出了一個(gè)非常強(qiáng)大的 idea,即在如此多的不同場(chǎng)景中檢查如此多的不同類型是多么的復(fù)雜和困難。
類型檢查器不依賴于調(diào)用代碼,即如果一個(gè)文件中的任何代碼被執(zhí)行(例如,在運(yùn)行時(shí))。類型檢查器將處理給定文件中的每一行,并運(yùn)行適當(dāng)?shù)臋z查。
高級(jí)類型檢查器功能
由于這些概念的復(fù)雜性,我們今天不深入探討以下幾個(gè)概念:
懶編譯 - Lazy compilation
現(xiàn)代編譯的一個(gè)共同特征是延遲加載。他們不會(huì)重新計(jì)算或重新編譯文件或 AST 分支,除非絕對(duì)需要。
TypeScript 預(yù)處理程序可以使用緩存在內(nèi)存中的前一次運(yùn)行的 AST 代碼。這將大大提高性能,因?yàn)樗恍枰P(guān)注程序或節(jié)點(diǎn)樹(shù)的一小部分已更改的內(nèi)容。
TypeScript 使用不可變的只讀數(shù)據(jù)結(jié)構(gòu),這些數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)在它所稱的 look aside tables 中。這樣很容易知道什么已經(jīng)改變,什么沒(méi)有改變。
穩(wěn)健性
在編譯時(shí),有些操作編譯器不確定是安全的,必須等待運(yùn)行時(shí)。每個(gè)編譯器都必須做出困難的選擇,以確定哪些內(nèi)容將被包含,哪些不會(huì)被包含。TypeScript 有一些被稱為不健全的區(qū)域(即需要運(yùn)行時(shí)類型檢查)。
我們不會(huì)在編譯器中討論上述特性,因?yàn)樗鼈冊(cè)黾恿祟~外的復(fù)雜性,對(duì)于我們的小 POC 來(lái)說(shuō)不值得。
現(xiàn)在令人興奮的是,我們自己也要實(shí)現(xiàn)一個(gè)編譯器。
B 部分:構(gòu)建我們自己的類型系統(tǒng)編譯器
我們將構(gòu)建一個(gè)編譯器,它可以對(duì)三個(gè)不同的場(chǎng)景運(yùn)行類型檢查,并為每個(gè)場(chǎng)景拋出特定的信息。
我們將其限制在三個(gè)場(chǎng)景中的原因是,我們可以關(guān)注每一個(gè)場(chǎng)景中的具體機(jī)制,并希望到最后能夠?qū)θ绾我敫鼜?fù)雜的類型檢查有一個(gè)更好的構(gòu)思。
我們將在編譯器中使用函數(shù)聲明和表達(dá)式(調(diào)用該函數(shù))。
這些場(chǎng)景包括:
1. 字符串與數(shù)字的類型匹配問(wèn)題
fn("craig-string"); // throw with string vs number
function fn(a: number) {}
2. 使用未定義的未知類型
fn("craig-string"); // throw with string vs ?
function fn(a: made_up_type) {} // throw with bad type
3. 使用代碼中未定義的屬性名
interface Person {
name: string;
}
fn({ nam: "craig" }); // throw with "nam" vs "name"
function fn(a: Person) {}
實(shí)現(xiàn)我們的編譯器,需要兩部分:解析器和檢查器。
解析器 - Parser
前面提到,我們今天不會(huì)關(guān)注解析器。我們將遵循 Hegel 的解析方法,假設(shè)一個(gè) typeAnnotation 對(duì)象已經(jīng)附加到所有帶注解的 AST 節(jié)點(diǎn)中。我已經(jīng)硬編碼了 AST 對(duì)象。
場(chǎng)景 1 將使用以下解析器:
字符串與數(shù)字的類型匹配問(wèn)題
function parser(code) {
// fn("craig-string");
const expressionAst = {
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "Identifier",
name: "fn"
},
arguments: [
{
type: "StringLiteral", // Parser "Inference" for type.
value: "craig-string"
}
]
}
};
// function fn(a: number) {}
const declarationAst = {
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "fn"
},
params: [
{
type: "Identifier",
name: "a",
// 參數(shù)標(biāo)識(shí)
typeAnnotation: {
// our only type annotation
type: "TypeAnnotation",
typeAnnotation: {
// 數(shù)字類型
type: "NumberTypeAnnotation"
}
}
}
],
body: {
type: "BlockStatement",
body: [] // "body" === block/line of code. Ours is empty
}
};
const programAst = {
type: "File",
program: {
type: "Program",
body: [expressionAst, declarationAst]
}
};
// normal AST except with typeAnnotations on
return programAst;
}
可以看到場(chǎng)景 1 中,第一行 fn("craig-string") 語(yǔ)句的 AST 對(duì)應(yīng) expressionAst,第二行聲明函數(shù)的 AST 對(duì)應(yīng) declarationAst。最后返回一個(gè) programmast,它是一個(gè)包含兩個(gè) AST 塊的程序。
在AST中,您可以看到參數(shù)標(biāo)識(shí)符 a 上的 typeAnnotation,與它在代碼中的位置相匹配。
場(chǎng)景 2 將使用以下解析器:
使用未定義的未知類型
function parser(code) {
// fn("craig-string");
const expressionAst = {
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "Identifier",
name: "fn"
},
arguments: [
{
type: "StringLiteral", // Parser "Inference" for type.
value: "craig-string"
}
]
}
};
// function fn(a: made_up_type) {}
const declarationAst = {
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "fn"
},
params: [
{
type: "Identifier",
name: "a",
typeAnnotation: {
// our only type annotation
type: "TypeAnnotation",
typeAnnotation: {
// 參數(shù)類型不同于場(chǎng)景 1
type: "made_up_type" // BREAKS
}
}
}
],
body: {
type: "BlockStatement",
body: [] // "body" === block/line of code. Ours is empty
}
};
const programAst = {
type: "File",
program: {
type: "Program",
body: [expressionAst, declarationAst]
}
};
// normal AST except with typeAnnotations on
return programAst;
}
場(chǎng)景 2 的解析器的表達(dá)式、聲明和程序 AST 塊非常類似于場(chǎng)景 1。然而,區(qū)別在于 params 內(nèi)部的 typeAnnotation 是 made_up_type,而不是場(chǎng)景 1 中的 NumberTypeAnnotation。
typeAnnotation: {
type: "made_up_type" // BREAKS
}
場(chǎng)景 3 使用以下解析器:
使用代碼中未定義的屬性名
function parser(code) {
// interface Person {
// name: string;
// }
const interfaceAst = {
type: "InterfaceDeclaration",
id: {
type: "Identifier",
name: "Person",
},
body: {
type: "ObjectTypeAnnotation",
properties: [
{
type: "ObjectTypeProperty",
key: {
type: "Identifier",
name: "name",
},
kind: "init",
method: false,
value: {
type: "StringTypeAnnotation",
},
},
],
},
};
// fn({nam: "craig"});
const expressionAst = {
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "Identifier",
name: "fn",
},
arguments: [
{
type: "ObjectExpression",
properties: [
{
type: "ObjectProperty",
method: false,
key: {
type: "Identifier",
name: "nam",
},
value: {
type: "StringLiteral",
value: "craig",
},
},
],
},
],
},
};
// function fn(a: Person) {}
const declarationAst = {
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "fn",
},
params: [
{
type: "Identifier",
name: "a",
//
typeAnnotation: {
type: "TypeAnnotation",
typeAnnotation: {
type: "GenericTypeAnnotation",
id: {
type: "Identifier",
name: "Person",
},
},
},
},
],
body: {
type: "BlockStatement",
body: [], // Empty function
},
};
const programAst = {
type: "File",
program: {
type: "Program",
body: [interfaceAst, expressionAst, declarationAst],
},
};
// normal AST except with typeAnnotations on
return programAst;
}
除了表達(dá)式、聲明和程序 AST 塊之外,還有一個(gè) interfaceAst 塊,它負(fù)責(zé)保存 InterfaceDeclaration AST。
在declarationAst 塊的 typeAnnotation 節(jié)點(diǎn)上有一個(gè) GenericType,因?yàn)樗邮芤粋€(gè)對(duì)象標(biāo)識(shí)符,即 Person。在這個(gè)場(chǎng)景中,programAst 將返回這三個(gè)對(duì)象的數(shù)組。
解析器的相似性
從上面可以得知,這三種有共同點(diǎn), 3 個(gè)場(chǎng)景中保存所有的類型注解的主要區(qū)域是 declaration。
檢查器
現(xiàn)在來(lái)看編譯器的類型檢查部分。
它需要遍歷所有程序主體的 AST 對(duì)象,并根據(jù)節(jié)點(diǎn)類型進(jìn)行適當(dāng)?shù)念愋蜋z查。我們將把所有錯(cuò)誤添加到一個(gè)數(shù)組中,并返回給調(diào)用者以便打印。
在我們進(jìn)一步討論之前,對(duì)于每種類型,我們將使用的基本邏輯是:
函數(shù)聲明:檢查參數(shù)的類型是否有效,然后檢查函數(shù)體中的每個(gè)語(yǔ)句。
表達(dá)式:找到被調(diào)用的函數(shù)聲明,獲取聲明上的參數(shù)類型,然后獲取函數(shù)調(diào)用表達(dá)式傳入的參數(shù)類型,并進(jìn)行比較。
代碼
以下代碼中包含 typeChecks 對(duì)象(和 errors 數(shù)組) ,它將用于表達(dá)式檢查和基本的注解(annotation)檢查。
const errors = [];
// 注解類型
const ANNOTATED_TYPES = {
NumberTypeAnnotation: "number",
GenericTypeAnnotation: true
};
// 類型檢查的邏輯
const typeChecks = {
// 比較形參和實(shí)參的類型
expression: (declarationFullType, callerFullArg) => {
switch (declarationFullType.typeAnnotation.type) {
// 注解為 number 類型
case "NumberTypeAnnotation":
// 如果調(diào)用時(shí)傳入的是數(shù)字,返回 true
return callerFullArg.type === "NumericLiteral";
// 注解為通用類型
case "GenericTypeAnnotation": // non-native
// 如果是對(duì)象,檢查對(duì)象的屬性
if (callerFullArg.type === "ObjectExpression") {
// 獲取接口節(jié)點(diǎn)
const interfaceNode = ast.program.body.find(
node => node.type === "InterfaceDeclaration"
);
const properties = interfaceNode.body.properties;
//遍歷檢查調(diào)用時(shí)的每個(gè)屬性
properties.map((prop, index) => {
const name = prop.key.name;
const associatedName = callerFullArg.properties[index].key.name;
// 沒(méi)有匹配,將錯(cuò)誤信息存入 errors
if (name !== associatedName) {
errors.push(
`Property "${associatedName}" does not exist on interface "${interfaceNode.id.name}". Did you mean Property "${name}"?`
);
}
});
}
return true; // as already logged
}
},
annotationCheck: arg => {
return !!ANNOTATED_TYPES[arg];
}
};
讓我們來(lái)看一下代碼,我們的 expression 有兩種類型的檢查:
對(duì)于
NumberTypeAnnotation;調(diào)用時(shí)類型應(yīng)為AnumericTeral(即,如果注解為數(shù)字,則調(diào)用時(shí)類型應(yīng)為數(shù)字)。場(chǎng)景1將在此處失敗,但未記錄任何錯(cuò)誤信息。對(duì)于
GenericTypeAnnotation;如果是一個(gè)對(duì)象,我們將在 AST 中查找InterfaceDeclaration節(jié)點(diǎn),然后檢查該接口上調(diào)用者的每個(gè)屬性。之后將所有錯(cuò)誤信息都會(huì)被存到errors數(shù)組中,場(chǎng)景3將在這里失敗并得到這個(gè)錯(cuò)誤。
我們的處理僅限于這個(gè)文件中,大多數(shù)類型檢查器都有作用域的概念,因此它們能夠確定聲明在運(yùn)行時(shí)的準(zhǔn)確位置。我們的工作更簡(jiǎn)單,因?yàn)樗皇且粋€(gè)
POC。
以下代碼包含程序體中每個(gè)節(jié)點(diǎn)類型的處理。這就是上面調(diào)用類型檢查邏輯的地方。
// Process program
ast.program.body.map(stnmt => {
switch (stnmt.type) {
case "FunctionDeclaration":
stnmt.params.map(arg => {
// Does arg has a type annotation?
if (arg.typeAnnotation) {
const argType = arg.typeAnnotation.typeAnnotation.type;
// Is type annotation valid
const isValid = typeChecks.annotationCheck(argType);
if (!isValid) {
errors.push(
`Type "${argType}" for argument "${arg.name}" does not exist`
);
}
}
});
// Process function "block" code here
stnmt.body.body.map(line => {
// Ours has none
});
return;
case "ExpressionStatement":
const functionCalled = stnmt.expression.callee.name;
const declationForName = ast.program.body.find(
node =>
node.type === "FunctionDeclaration" &&
node.id.name === functionCalled
);
// Get declaration
if (!declationForName) {
errors.push(`Function "${functionCalled}" does not exist`);
return;
}
// Array of arg-to-type. e.g. 0 = NumberTypeAnnotation
const argTypeMap = declationForName.params.map(param => {
if (param.typeAnnotation) {
return param.typeAnnotation;
}
});
// Check exp caller "arg type" with declaration "arg type"
stnmt.expression.arguments.map((arg, index) => {
const declarationType = argTypeMap[index].typeAnnotation.type;
const callerType = arg.type;
const callerValue = arg.value;
// Declaration annotation more important here
const isValid = typeChecks.expression(
argTypeMap[index], // declaration details
arg // caller details
);
if (!isValid) {
const annotatedType = ANNOTATED_TYPES[declarationType];
// Show values to user, more explanatory than types
errors.push(
`Type "${callerValue}" is incompatible with "${annotatedType}"`
);
}
});
return;
}
});
讓我們?cè)俅伪闅v代碼,按類型對(duì)其進(jìn)行分解。
FunctionDeclaration (即 function hello(){})
首先處理 arguments/params。如果找到類型注解,就檢查給定參數(shù)的類型 argType 是否存在。如果不進(jìn)行錯(cuò)誤處理,場(chǎng)景 2 會(huì)在這里報(bào)錯(cuò)誤。
之后處理函數(shù)體,但是我們知道沒(méi)有函數(shù)體需要處理,所以我把它留空了。
stnmt.body.body.map(line => {
// Ours has none
});
ExpressionStatement (即 hello())
首先檢查程序中函數(shù)的聲明。這就是作用域?qū)?yīng)用于實(shí)際類型檢查器的地方。如果找不到聲明,就將錯(cuò)誤信息添加到 errors 數(shù)組中。
接下來(lái),我們針對(duì)調(diào)用時(shí)傳入的參數(shù)類型(實(shí)參類型)檢查每個(gè)已定義的參數(shù)類型。如果發(fā)現(xiàn)類型不匹配,則向 errors 數(shù)組中添加一個(gè)錯(cuò)誤。場(chǎng)景 1 和場(chǎng)景 2 在這里都會(huì)報(bào)錯(cuò)。
運(yùn)行我們的編譯器
源碼存放在這里,該文件一次性處理所有三個(gè) AST 節(jié)點(diǎn)對(duì)象并記錄錯(cuò)誤。
運(yùn)行它時(shí),我得到以下信息:

總而言之:
場(chǎng)景 1:
fn("craig-string"); // throw with string vs number
function fn(a: number) {}
我們定義參數(shù)為 number 的類型,然后用字符串調(diào)用它。
場(chǎng)景 2:
fn("craig-string"); // throw with string vs ?
function fn(a: made_up_type) {} // throw with bad type
我們?cè)诤瘮?shù)參數(shù)上定義了一個(gè)不存在的類型,然后調(diào)用我們的函數(shù),所以我們得到了兩個(gè)錯(cuò)誤(一個(gè)是定義的錯(cuò)誤類型,另一個(gè)是類型不匹配的錯(cuò)誤)。
場(chǎng)景 3:
interface Person {
name: string;
}
fn({ nam: "craig" }); // throw with "nam" vs "name"
function fn(a: Person) {}
我們定義了一個(gè)接口,但是使用了一個(gè)名為 nam 的屬性,這個(gè)屬性不在對(duì)象上,錯(cuò)誤提示我們是否要使用 name。
我們遺漏了什么?
如前所述,類型編譯器還有許多其他部分,我們?cè)诰幾g器中省略了這些部分。其中包括:
解析器:我們是手動(dòng)編寫的 AST 代碼,它們實(shí)際上是在類型的編譯器上解析生成。 預(yù)處理/語(yǔ)言編譯器: 一個(gè)真正的編譯器具有插入 IDE 并在適當(dāng)?shù)臅r(shí)候重新運(yùn)行的機(jī)制。 懶編譯:沒(méi)有關(guān)于更改或內(nèi)存使用的信息。 轉(zhuǎn)換:我們跳過(guò)了編譯器的最后一部分,也就是生成本機(jī) JavaScript 代碼的地方。 作用域:因?yàn)槲覀兊?POC 是一個(gè)單一的文件,它不需要理解作用域的概念,但是真正的編譯器必須始終知道上下文。
非常感謝您的閱讀和觀看,我從這項(xiàng)研究中了解了大量關(guān)于類型系統(tǒng)的知識(shí),希望對(duì)您有所幫助。以上完整代碼您可以在這里找到。(給原作者 start)
備注:
原作者在源碼中使用的 Node 模塊方式為 ESM(ES Module),在將源碼克隆到本地后,如果運(yùn)行不成功,需要修改 start 指令,添加啟動(dòng)參數(shù) --experimental-modules。
"start": "node --experimental-modules src/index.mjs",
END


“分享、點(diǎn)贊、在看” 支持一波
