Typescript 类型检查原理之Override 是如何实现的

开发 前端
前段时间写过一篇类型检查的实现原理的文章,实现了简单的赋值语句和函数调用的类型检查。实际上类型检查的情况特别多,一篇文章肯定写不完,所以我准备用系列文章来讲述各种类型检查的实现原理,帮助大家更好的掌握 typescript。

[[403862]]

本文转载自微信公众号「神光的编程秘籍」,作者神说要有光 。转载本文请联系神光的编程秘籍公众号。

前段时间写过一篇类型检查的实现原理的文章,实现了简单的赋值语句和函数调用的类型检查。实际上类型检查的情况特别多,一篇文章肯定写不完,所以我准备用系列文章来讲述各种类型检查的实现原理,帮助大家更好的掌握 typescript。

这一篇我们来实现 4.3 新增的 class 的 override 关键字的类型检查。(源码链接在后面)

override 修饰符是干嘛的

首先,我们来看下这个修饰符的作用:被 override 标示的方法必须得在父类中存在,否则会报错。

  1. class Animal { 
  2.   getName() { return ''; } 
  3. class Dog extends Animal { 
  4.   override bak() { 
  5.     return 'wang'
  6.   } 
  7.   override getName() { 
  8.     return 'wang'
  9.   } 

上面这段代码会报错:This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'.就是说重写的放在父类不存在,这样能避免父类重构的时候把一些子类需要重写的方法给去掉。

如何实现 override 修饰符的类型检查

其实所有的修饰符,包括 override、public、static 等,在 parse 成 AST 后都是作为一个属性存在的,这个 override 也是,我们通过 astexplorer.net 来查看一下。

可以看到 override 属性为 true。这样我们就可以通过这个属性把该 class 的所有的需要 override 的 ClassMethod 过滤出来。

然后还可以拿到 superClass 的名字,从作用域中找到对应的声明,然后遍历 AST 找到它所声明的所有 ClassMethod。

两者对比一下,所有不在父类中的 ClassMethod 都需要报错。

代码实现

我们基于 babel 来做 parser 和分析,写一个插件来做 override 的类型检查。关于 babel 插件的基础可以看小册《babel 插件通关秘籍》。

开启语法 typescript 插件来解析 ts 语法。

  1. const { transformFromAstSync } = require('@babel/core'); 
  2. const  parser = require('@babel/parser'); 
  3.  
  4. const ast = parser.parse(sourceCode, { 
  5.     sourceType: 'unambiguous'
  6.     plugins: ['typescript'
  7. }); 
  8.  
  9. const { code } = transformFromAstSync(ast, sourceCode, { 
  10.     plugins: [overrideCheckerPlugin] 
  11. }); 

插件要处理的是 ClassDeclaration,我们先搭一个基本的结构:

  1. const { declare } = require('@babel/helper-plugin-utils'); 
  2.  
  3. const overrideCheckerPlugin = declare((api, options, dirname) => { 
  4.     api.assertVersion(7); 
  5.  
  6.     return { 
  7.         pre(file) { 
  8.             file.set('errors', []); 
  9.         }, 
  10.         visitor: { 
  11.             ClassDeclaration(path, state) { 
  12.                 const semanticErrors = state.file.get('errors'); 
  13.                 //... 
  14.                 state.file.set('errors', semanticErrors); 
  15.             } 
  16.         }, 
  17.         post(file) { 
  18.             console.log(file.get('errors')); 
  19.         } 
  20.     } 
  21. }); 

具体的检查逻辑是拿到父类的所有方法名,拿到当前类的所有 override 方法名,然后做下过滤。

我们首先要拿到父类的 ast,通过名字从作用域中查找。

  1. const superClass = path.node.superClass; 
  2. if (superClass) { 
  3.     const superClassPath = path.scope.getBinding(superClass.name).path; 

然后封装一个方法来拿父类方法名,通过 path.traverse 来遍历 ast,把收集到的方法名存到 state 中。

  1. function getAllClassMethodNames(classDeclarationNodePath) { 
  2.     const state = { 
  3.         allSuperMethodNames: [] 
  4.     } 
  5.     classDeclarationNodePath.traverse({ 
  6.         ClassMethod(path) { 
  7.             state.allSuperMethodNames.push(path.get('key').toString()) 
  8.         } 
  9.     }); 
  10.     return state.allSuperMethodNames; 

这样就拿到了所有父类方法名。

之后需要拿到当前类的所有方法名并过滤出 override 为 true 且不在父类中的进行报错。

  1. const superClass = path.node.superClass; 
  2. if (superClass) { 
  3.     const superClassPath = path.scope.getBinding(superClass.name).path; 
  4.     const allMethodNames = getAllClassMethodNames(superClassPath); 
  5.  
  6.     path.traverse({ 
  7.         ClassMethod(path) { 
  8.             if (path.node.override){ 
  9.                 const methodName = path.get('key').toString(); 
  10.                 const superClassName = superClassPath.get('id').toString(); 
  11.                 if (!allMethodNames.includes(methodName)) { 
  12.                     // 报错                                     
  13.                 } 
  14.             } 
  15.         } 
  16.     }); 

报错的部分使用 code frame 来创建友好的代码打印格式,通过 Error.stackTraceLimit 设置为 0 去掉调用栈信息。

  1. const tmp = Error.stackTraceLimit; 
  2. Error.stackTraceLimit = 0; 
  3. let errorMessage = `this member cannot have an 'override' modifier because it is not declared in the base class '${superClassName}'`; 
  4. semanticErrors.push(path.get('key').buildCodeFrameError(errorMessage, Error)); 
  5. Error.stackTraceLimit = tmp; 

这样,我们就完成了 override 的类型检查,整体代码如下:

  1. const { declare } = require('@babel/helper-plugin-utils'); 
  2.  
  3. function getAllClassMethodNames(classDeclarationNodePath) { 
  4.     const state = { 
  5.         allSuperMethodNames: [] 
  6.     } 
  7.     classDeclarationNodePath.traverse({ 
  8.         ClassMethod(path) { 
  9.             state.allSuperMethodNames.push(path.get('key').toString()) 
  10.         } 
  11.     }); 
  12.     return state.allSuperMethodNames; 
  13.  
  14. const overrideCheckerPlugin = declare((api, options, dirname) => { 
  15.     api.assertVersion(7); 
  16.  
  17.     return { 
  18.         pre(file) { 
  19.             file.set('errors', []); 
  20.         }, 
  21.         visitor: { 
  22.             ClassDeclaration(path, state) { 
  23.                 const semanticErrors = state.file.get('errors'); 
  24.  
  25.                 const superClass = path.node.superClass; 
  26.                 if (superClass) { 
  27.                     const superClassPath = path.scope.getBinding(superClass.name).path; 
  28.                     const allMethodNames = getAllClassMethodNames(superClassPath); 
  29.          
  30.                     path.traverse({ 
  31.                         ClassMethod(path) { 
  32.                             if (path.node.override){ 
  33.                                 const methodName = path.get('key').toString(); 
  34.                                 const superClassName = superClassPath.get('id').toString(); 
  35.                                 if (!allMethodNames.includes(methodName)) { 
  36.                                     const tmp = Error.stackTraceLimit; 
  37.                                     Error.stackTraceLimit = 0; 
  38.                                     let errorMessage = `this member cannot have an 'override' modifier because it is not declared in the base class '${superClassName}'`; 
  39.                                     semanticErrors.push(path.get('key').buildCodeFrameError(errorMessage, Error)); 
  40.                                     Error.stackTraceLimit = tmp;                                     
  41.                                 } 
  42.                             } 
  43.                         } 
  44.                     }); 
  45.                 } 
  46.                 state.file.set('errors', semanticErrors); 
  47.             } 
  48.         }, 
  49.         post(file) { 
  50.             console.log(file.get('errors')); 
  51.         } 
  52.     } 
  53. }); 
  54.  
  55. module.exports = overrideCheckerPlugin; 

github 链接

测试效果

我们用最开始的代码来测试一下:

  1. class Animal { 
  2.     getName() { return ''; } 
  3. class Dog extends Animal { 
  4.     override bak() { 
  5.         return 'wang'
  6.     } 
  7.     override getName() { 
  8.         return 'wang'
  9.     } 

打印信息为:

正确的识别出了 bak 在父类不存在的错误。

至此,我们实现了 override 的类型检查!

总结

类型检查情况很多,所以需要一个系列文章去讲,这一篇我们来实现 override 的类型检查。

override 是 ts 4.3 加入的特性,带有 override 修饰符的方法必须在父类中有对应的声明,否则会报错。

我们通过 babel 插件的方式实现了类型检查,思路是从作用域取出父类的声明,然后通过 path.traverse 拿到所有方法名,之后再取当前类的所有方法名,对于没在父类中声明并且带有 override 修饰符的方法进行报错。

本文是 【typescript 类型检查原理】系列文章的第二篇,后续还会有更多 typescript 类型检查的实现原理揭秘的文章。希望能够帮助大家更好的掌握 typescript。

关于 babel 插件的知识,可以看我的 babel 小册《babel 插件通关秘籍》,其中有详细的讲解。

本文源码链接 https://github.com/QuarkGluonPlasma/babel-plugin-exercize/tree/master/exercize-type-checker/src

 

责任编辑:武晓燕 来源: 神光的编程秘籍
相关推荐

2021-06-09 07:55:19

Typescript类型检查

2022-04-11 08:42:09

TypeScript子类型定义

2022-05-04 09:02:41

TypeScript类型工具

2009-07-22 09:43:30

Scala类型

2022-04-10 19:26:07

TypeScript类型语法

2020-12-18 11:35:22

TypeScript语言Java

2022-09-14 15:24:57

typescript快排

2023-01-05 08:09:27

GroovyDSL​

2023-06-13 18:24:26

TypeScriptJSDoc开发

2022-02-25 09:06:02

TypeScripnever工具

2012-07-02 10:43:49

JVMGroovyJava

2021-07-27 06:06:34

TypeScript语言运算符

2013-07-09 14:41:58

C动态类型

2022-02-09 08:11:50

架构

2021-08-18 07:56:05

Typescript类型本质

2022-08-08 09:00:42

TypeScript映射类型

2011-08-15 10:16:55

内存泄露

2022-08-12 16:12:34

JavaScript数据类型字符串

2022-09-20 14:43:55

TypeScript类型体操

2021-09-17 12:50:10

MySQL数据库ACID
点赞
收藏

51CTO技术栈公众号