typescript 给 javascript 扩展了类型的语法和语义,让咱们能够给变量、函数等定义类型,而后编译期间检查,这样可以提早发现类型不匹配的错误,还可以在开发时提示可用的属性方法。javascript
并且,typescript 并不像当年的 coffeescript 同样改变了语法,它是 javascript 的一个超集,只作了类型的扩展。前端
这些优势使得 typescript 迅速的火了起来。如今前端面试若是你不会 typescript,那么可能很难拿到 offer。java
市面上关于 typescript 的教程文不少了,可是没有一篇去从编译原理的角度分析它的实现的。本文不会讲 typescript 的基础,而是会实现一个 typescript type checker,帮你理解类型检查究竟作了什么。理解了类型检查的实现思路,再去学 typescript,或许就没那么难了。node
typescript compiler 是一个 转译器,负责把 typescript 的语法转成 es201五、es五、es3 的目标 javascript,而且过程当中会作类型检查。面试
babel 也是一个转译器,能够把 es next、typescript、flow 等语法转成目标环境支持的 js。typescript
babel 也能够编译 typescript? 对的,babel 7 之后就能够编译 typescript 代码,这仍是 typescript 团队和 babel 团队合做一年的成果。api
咱们知道,babel 编译流程分为 3 个步骤:parse、transform、generate。babel
parse 阶段负责编译源码成 AST,transform 阶段对 AST 进行增删改,generate 阶段打印 AST 成目标代码并生成 sorucemap。markdown
babel 能够编译 typescript 代码只是可以 parse,并不会作类型检查,咱们彻底能够基于 babel parse 出的 AST 来实现一下类型检查。app
咱们常常用 tsc 来作类型检查,有没有想过,类型检查具体作了什么?
类型表明了变量存储的内容,也就是规定了这块内容占据多大的内存空间,能够对它作什么操做。好比 number 和 boolean 就会分配不一样字节数的内存,Date 和 String 能够调用的方法也不一样。这就是类型的做用。它表明了一种可能性,你能够在这块内存放多少内容,可能对它进行什么操做。
动态类型是指类型是在运行时肯定的,而静态类型是指编译期间就知道了变量的类型信息,有了类型信息天然就知道了对它而言什么操做是合法的,什么操做是不合法的,什么变量可以赋值给他。
静态类型会在代码中保留类型信息,这个类型信息多是显式声明的,也多是自动推导出来的。想作一个大的项目,没有静态类型来约束和提早检查代码的话,太容易出 bug 了,会很难维护。这也是随着前端项目逐渐变得复杂,出现了 typescript 以及 typescript 愈来愈火的缘由。
咱们知道了什么是类型,为何要作静态的类型检查,那么怎么检查呢?
检查类型就是检查变量的内容,而理解代码的话须要把代码 parse 成 AST,因此类型检查也就变成了对 AST 结构的检查。
好比一个变量声明为了 number,那么给它赋值的是一个 string 就是有类型错误。
再复杂一点,若是类型有泛型,也就是有类型参数,那么须要传入具体的参数来肯定类型,肯定了类型以后再去和实际的 AST 对比。
typescript 还支持高级类型,也就是类型能够作各类运算,这种就须要传入类型参数求出具体的类型再去和 AST 对比。
咱们来写代码实现一下:
好比这样一段代码,声明的值是一个 string,可是赋值为了 number,明显是有类型错误的,咱们怎么检查出它的错误的。
let name: string;
name = 111;
复制代码
首先咱们使用 babel 把这段代码 parse 成 AST:
const parser = require('@babel/parser');
const sourceCode = ` let name: string; name = 111; `;
const ast = parser.parse(sourceCode, {
plugins: ['typescript']
});
复制代码
使用 babel parser 来 parse,启用 typescript 语法插件。
可使用 astexplerer.net 来查看它的 AST:
咱们须要检查的是这个赋值语句 AssignmentExpression,左右两边的类型是否匹配。
右边是一个数字字面量 NumericLiteral,很容易拿到类型,而左边则是一个引用,要从做用域中拿到它声明的类型,以后才能作类型对比。
babel 提供了 scope 的 api 能够用于查找做用域中的类型声明(binding),而且还能够经过 getTypeAnnotation 得到声明时的类型
AssignmentExpression(path, state) {
const leftBinding = path.scope.getBinding(path.get('left'));
const leftType = leftBinding.path.get('id').getTypeAnnotation();// 左边的值声明的类型
}
复制代码
这个返回的类型是 TSTypeAnnotation 的一个对象,咱们须要作下处理,转为类型字符串
封装一个方法,传入类型对象,返回 number、string 等类型字符串
function resolveType(targetType) {
const tsTypeAnnotationMap = {
'TSStringKeyword': 'string'
}
switch (targetType.type) {
case 'TSTypeAnnotation':
return tsTypeAnnotationMap[targetType.typeAnnotation.type];
case 'NumberTypeAnnotation':
return 'number';
}
}
复制代码
这样咱们拿到了左右两边的类型,接下来就简单了,对比下就知道了类型是否匹配:
AssignmentExpression(path, state) {
const rightType = resolveType(path.get('right').getTypeAnnotation());
const leftBinding = path.scope.getBinding(path.get('left'));
const leftType = resolveType(leftBinding.path.get('id').getTypeAnnotation());
if (leftType !== rightType ) {
// error: 类型不匹配
}
}
复制代码
报错信息怎么打印呢?可使用 @babel/code-frame,它支持打印某一片断的高亮代码。
path.get('right').buildCodeFrameError(`${rightType} can not assign to ${leftType}`, Error)
复制代码
效果以下:
这个错误堆栈也太丑了,咱们把它去掉,设置 Error.stackTraceLimit 为 0 就好了
Error.stackTraceLimit = 0;
path.get('right').buildCodeFrameError(`${rightType} can not assign to ${leftType}`, Error));
复制代码
可是这里改了以后还要改回来,也就是:
const tmp = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
console.log(path.get('right').buildCodeFrameError(`${rightType} can not assign to ${leftType}`, Error));
Error.stackTraceLimit = tmp;
复制代码
再来跑一下:
好看多了!
还有一个问题,如今是遇到类型错误就报错,但咱们但愿是在遇到类型错误时收集起来,最后统一报错。
怎么实现呢?错误放在哪?
babel 插件中能够拿到 file 对象,有 set 和 get 方法用来存取一些全局的信息。能够在插件调用先后,也就是 pre 和 post 阶段拿到 file 对象(这些在掘金小册《babel 插件通关秘籍》中会细讲)。
因此咱们能够这样作:
pre(file) {
file.set('errors', []);
},
visitor: {
AssignmentExpression(path, state) {
const errors = state.file.get('errors');
const rightType = resolveType(path.get('right').getTypeAnnotation());
const leftBinding = path.scope.getBinding(path.get('left'));
const leftType = resolveType(leftBinding.path.get('id').getTypeAnnotation());
if (leftType !== rightType ) {
const tmp = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
errors.push(path.get('right').buildCodeFrameError(`${rightType} can not assign to ${leftType}`, Error));
Error.stackTraceLimit = tmp;
}
}
},
post(file) {
console.log(file.get('errors'));
}
复制代码
这样就能够作到过程当中收集错误,最后统一打印:
这样,咱们就实现了简单的赋值语句的类型检查。
赋值语句的检查比较简单,咱们来进阶一下,实现函数调用参数的类型检查
function add(a: number, b: number): number{
return a + b;
}
add(1, '2');
复制代码
这里咱们要检查的就是函数调用语句 CallExpression 的参数和它声明的是否一致。
CallExpression 有 callee 和 arguments 两部分,咱们须要根据 callee 从做用域中查找函数声明,而后再把 arguments 的类型和函数声明语句的 params 的类型进行逐一对比,这样就实现了函数调用参数的类型检查。
pre(file) {
file.set('errors', []);
},
visitor: {
CallExpression(path, state) {
const errors = state.file.get('errors');
// 调用参数的类型
const argumentsTypes = path.get('arguments').map(item => {
return resolveType(item.getTypeAnnotation());
});
const calleeName = path.get('callee').toString();
// 根据 callee 查找函数声明
const functionDeclarePath = path.scope.getBinding(calleeName).path;
// 拿到声明时参数的类型
const declareParamsTypes = functionDeclarePath.get('params').map(item => {
return resolveType(item.getTypeAnnotation());
})
argumentsTypes.forEach((item, index) => {
if (item !== declareParamsTypes[index]) {
// 类型不一致,报错
}
});
}
},
post(file) {
console.log(file.get('errors'));
}
复制代码
运行一下,效果以下:
咱们实现了函数调用参数的类型检查!实际上思路仍是挺清晰的,检查别的 AST 也是相似的思路。
泛型是什么,其实就是类型参数,使得类型能够根据传入的参数动态肯定,类型定义更加灵活。
好比这样一段代码:
function add<T>(a: T, b: T) {
return a + b;
}
add<number>(1, '2');
复制代码
怎么作类型检查呢?
这仍是函数调用语句的类型检查,咱们上面实现过了,区别不过是多了个参数,那么咱们取出类型参数来传过去就好了。
CallExpression(path, state) {
const realTypes = path.node.typeParameters.params.map(item => {// 先拿到类型参数的值,也就是真实类型
return resolveType(item);
});
const argumentsTypes = path.get('arguments').map(item => {
return resolveType(item.getTypeAnnotation());
});
const calleeName = path.get('callee').toString();
const functionDeclarePath = path.scope.getBinding(calleeName).path;
const realTypeMap = {};
functionDeclarePath.node.typeParameters.params.map((item, index) => {
realTypeMap[item.name] = realTypes[index];
});
const declareParamsTypes = functionDeclarePath.get('params').map(item => {
return resolveType(item.getTypeAnnotation(), realTypeMap);
})// 把类型参数的值赋值给函数声明语句的泛型参数
argumentsTypes.forEach((item, index) => { // 作类型检查的时候取具体的类型来对比
if (item !== declareParamsTypes[index]) {
// 报错,类型不一致
}
});
}
复制代码
多了一步肯定泛型参数的具体类型的过程。
执行看下效果:
咱们成功支持了带泛型的函数调用语句的类型检查!
typescript 支持高级类型,也就是支持对类型参数作各类运算而后返回最终类型
type Res<Param> = Param extends 1 ? number : string;
function add<T>(a: T, b: T) {
return a + b;
}
add<Res<1>>(1, '2');
复制代码
好比这段代码中,Res 就是一个高级类型,对传入的类型参数 Param 进行处理以后返回新类型。
这个函数调用语句的类型检查,比泛型参数传具体的类型又复杂了一些,须要先求出具体的类型,而后再传入参数,以后再去对比参数的类型。
那么这个 Res 的高级类型怎么求值呢?
咱们来看一下这个 Res 类型的 AST:
它有类型参数部分(typeParameters),和具体的类型计算逻辑部分(typeAnnotation),右边的 Param extends 1 ? number : string;
是一个 condition 语句,有 Params 和 1 分别对应 checkType、extendsType,number 和 string 则分别对应 trueType、falseType。
咱们只须要对传入的 Param 判断下是不是 1,就能够求出具体的类型是 trueType 仍是 falseType。
具体类型传参的逻辑和上面同样,就不赘述了,咱们看一下根据类型参数来值的逻辑:
function typeEval(node, params) {
let checkType;
if(node.checkType.type === 'TSTypeReference') {
checkType = params[node.checkType.typeName.name];// 若是参数是泛型,则从传入的参数取值
} else {
checkType = resolveType(node.checkType); // 不然直接取字面量参数
}
const extendsType = resolveType(node.extendsType);
if (checkType === extendsType || checkType instanceof extendsType) { // 若是 extends 逻辑成立
return resolveType(node.trueType);
} else {
return resolveType(node.falseType);
}
}
复制代码
这样,咱们就能够求出这个 Res 的高级类型当传入 Params 为 1 时求出的最终类型。
有了最终类型以后,就和直接传入具体类型的函数调用的类型检查同样了。(上面咱们实现过)
执行一下,效果以下:
完整代码以下(有些长,能够先跳过日后看):
const { declare } = require('@babel/helper-plugin-utils');
function typeEval(node, params) {
let checkType;
if(node.checkType.type === 'TSTypeReference') {
checkType = params[node.checkType.typeName.name];
} else {
checkType = resolveType(node.checkType);
}
const extendsType = resolveType(node.extendsType);
if (checkType === extendsType || checkType instanceof extendsType) {
return resolveType(node.trueType);
} else {
return resolveType(node.falseType);
}
}
function resolveType(targetType, referenceTypesMap = {}, scope) {
const tsTypeAnnotationMap = {
TSStringKeyword: 'string',
TSNumberKeyword: 'number'
}
switch (targetType.type) {
case 'TSTypeAnnotation':
if (targetType.typeAnnotation.type === 'TSTypeReference') {
return referenceTypesMap[targetType.typeAnnotation.typeName.name]
}
return tsTypeAnnotationMap[targetType.typeAnnotation.type];
case 'NumberTypeAnnotation':
return 'number';
case 'StringTypeAnnotation':
return 'string';
case 'TSNumberKeyword':
return 'number';
case 'TSTypeReference':
const typeAlias = scope.getData(targetType.typeName.name);
const paramTypes = targetType.typeParameters.params.map(item => {
return resolveType(item);
});
const params = typeAlias.paramNames.reduce((obj, name, index) => {
obj[name] = paramTypes[index];
return obj;
},{});
return typeEval(typeAlias.body, params);
case 'TSLiteralType':
return targetType.literal.value;
}
}
function noStackTraceWrapper(cb) {
const tmp = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
cb && cb(Error);
Error.stackTraceLimit = tmp;
}
const noFuncAssignLint = declare((api, options, dirname) => {
api.assertVersion(7);
return {
pre(file) {
file.set('errors', []);
},
visitor: {
TSTypeAliasDeclaration(path) {
path.scope.setData(path.get('id').toString(), {
paramNames: path.node.typeParameters.params.map(item => {
return item.name;
}),
body: path.getTypeAnnotation()
});
path.scope.setData(path.get('params'))
},
CallExpression(path, state) {
const errors = state.file.get('errors');
const realTypes = path.node.typeParameters.params.map(item => {
return resolveType(item, {}, path.scope);
});
const argumentsTypes = path.get('arguments').map(item => {
return resolveType(item.getTypeAnnotation());
});
const calleeName = path.get('callee').toString();
const functionDeclarePath = path.scope.getBinding(calleeName).path;
const realTypeMap = {};
functionDeclarePath.node.typeParameters.params.map((item, index) => {
realTypeMap[item.name] = realTypes[index];
});
const declareParamsTypes = functionDeclarePath.get('params').map(item => {
return resolveType(item.getTypeAnnotation(), realTypeMap);
})
argumentsTypes.forEach((item, index) => {
if (item !== declareParamsTypes[index]) {
noStackTraceWrapper(Error => {
errors.push(path.get('arguments.' + index ).buildCodeFrameError(`${item} can not assign to ${declareParamsTypes[index]}`,Error));
});
}
});
}
},
post(file) {
console.log(file.get('errors'));
}
}
});
module.exports = noFuncAssignLint;
复制代码
就这样,咱们实现了 typescript 高级类型!
类型表明了变量的内容和能对它进行的操做,静态类型让检查能够在编译期间作,随着前端项目愈来愈重,愈来愈须要 typescript 这类静态类型语言。
类型检查就是作 AST 的对比,判断声明的和实际的是否一致:
实现一个完整的 typescript type cheker 仍是很复杂的,否则 typescript checker 部分的代码也不至于好几万行了。可是思路其实没有那么难,按照咱们文中的思路来,是能够实现一个完整的 type checker 的。
(关于 babel 插件和 api 的部分,若是看不懂,能够在我即将上线的小册《babel 插件通关秘籍》中来详细了解。掌握了 babel,也就掌握了静态分析的能力,linter、type checker 这些顺带也能更深刻的掌握。)