Babel插件开发小试:基础Typescript支持

Stone926 Lv1

github仓库地址:stone926/babel-plugin-operator: A babel plugin to enable operator overloading in javascript

在最新版中,无论TS还是JS,重载函数有三种写法:object method、箭头函数、普通函数,即:

1
2
3
plus(l, r) {}
plus: (l, r) => {}
plus: function(l, r) {}

声明带某种运算对特定类型的重载,也支持自定义类型:

1
2
3
4
5
6
7
8
9
10
const $operator: OperatorObject = {
plus: [
(l: number, r: number): number => l + r,
(l: Matrix, r: Matrix): Matrix => l,
function (l: string, r: string): number {
return l.length + r.length;
},
],
minus: (l: string, r: number) => l.slice(r),
};

但是声明对联合类型(如:string|number|boolean)的重载会抛出TypeError,因为会导致混乱。

对插件来说只有重载函数参数的类型才有意义,返回值类型是可有可无的。如果重载函数没有注明类型,则视为any。只有当类型严格相同时才会重载,也就是说参数类型为(any, any)的函数不会重载(number, number)。重载当且仅当类型严格相等。

Babel编译Typescript的方式是移除一切类型检查,Babel不做任何类型分析。所以在插件中只能判断两类节点的类型:LiteralIdentifierIdentifier有两种:undefined和变量名。undefined的类型是undefined;对于变量名,可以在scope中的binding中找到其声明时的identifier,其中可以找到typeAnnotation节点;对于Literal,只需要判断是哪种Literal就可以确定类型。因此我们可以写出如下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export const getType = (node, scope) => {
if (t.isIdentifier(node)) {
if (node.name === "undefined") return t.tsUndefinedKeyword();
const binding = scope.getBinding(node.name);
return binding?.identifier.typeAnnotation?.typeAnnotation;
} else if (t.isStringLiteral(node) || t.isTemplateLiteral(node)) {
return t.tsStringKeyword();
} else if (t.isNumericLiteral(node)) {
return t.tsNumberKeyword();
} else if (t.isNullLiteral(node)) {
return t.tsNullKeyword();
} else if (t.isBooleanLiteral(node)) {
return t.tsBooleanKeyword();
} else if (t.isRegExpLiteral(node)) {
return t.tsTypeReference(t.identifier("RegExp"));
} else if (t.isBigIntLiteral(node)) {
return t.tsBigIntKeyword();
} else if (t.isDecimalLiteral(node)) { // !! what's this?
return t.tsNumberKeyword();
} else if (t.isUnaryExpression(node)) {
return node.operator === '-' && t.isNumericLiteral(node.argument) ? t.tsNumberKeyword() : undefined;
}
}

其中decimalLiteral是什么我并不清楚也没有查到,暂时当作number类型处理。需要注意,-1其实是一个UnaryExpression,其运算符operator-argumentNumericLiteral1。需要特殊判断负号表达式将其视为number类型。

得到类型后,我们判断运算符操作对象的类型和重载函数参数的类型就可以判定是否要重载此处的运算。我们将visitorFactory改造成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const visitorFactory = (replacement, typeKeys, tail = () => "") => (path) => {
const operatorObjectParent = path.findParent((parentPath) =>
t.isVariableDeclaration(parentPath) && operatorObjName == parentPath.node.declarations?.[0].id.name
);
if (operatorObjectParent) return;
let key = path.node.operator;
key += tail(path);
const operator = outer.registeredOperators.get(key);
if (operator) {
let replacer;
if (outer.isTs) {
const types = operator.types;
types.forEach((type, index) => {
let allSameType = true;
typeKeys.forEach((typeKey) => {
allSameType = allSameType && isSameType(getType(path.node[typeKey], path.scope),
type[typeKey]);
})
if (allSameType) {
if (type.index == -1) {
replacer = replacement(build(operatorObjName)[operator], path);
} else {
replacer = replacement(build(operatorObjName)[operator][index], path);
}
}
})
} else {
replacer = replacement(build(operatorObjName)[operator], path);
}
if (replacer) path.replaceWith(replacer[build.raw] ?? replacer);
}
};

其中,operator.types是我们在VariableDeclaration这个visitor中添加的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export const tsVariableDeclarationVisitor = (outer) => (path) => {
const name = path.node.declarations?.[0].id.name;
if (name === outer.operatorObjectName) {
path.node.declarations?.[0].init.properties.forEach(item => {
const name = new String(item.key.name);
name.types = [];
if (isFunctionOverloader(item)) {
const functionNode = t.isObjectMethod(item) ? item : item.value;
name.types.push(buildType(functionNode, path.buildCodeFrameError));
} else if (isArrayOverloader(item)) {
item.value.elements.forEach((functionNode, index) => {
t.assertFunction(functionNode);
name.types.push(buildType(functionNode, path.buildCodeFrameError, index));
});
}
registerOperator(outer.registeredOperators, name);
});
}
};

const buildType = (functionNode, err, index = -1) => {
const typeAnnotated = {}, anyTypeAnnotation = t.tsAnyKeyword();
const paramLength = functionNode.params.length;
if (paramLength == 2) {
typeAnnotated.left = functionNode.params[0].typeAnnotation?.typeAnnotation ?? anyTypeAnnotation;
typeAnnotated.right = functionNode.params[1].typeAnnotation?.typeAnnotation ?? anyTypeAnnotation;
} else if (paramLength == 1) {
typeAnnotated.argument = functionNode.params[0].typeAnnotation.typeAnnotation;
} else {
throw err(`Invalid Params Count. Expected 1 or 2, but got ${paramLength}`);
}
typeAnnotated.return = functionNode.returnType?.typeAnnotation ?? anyTypeAnnotation;
typeAnnotated.index = index;
return typeAnnotated;
}

新的visitor如下,以UpdateExpression为例,原先其写法过于冗长:

1
2
3
4
5
6
7
8
9
10
{
UpdateExpression: visitorFactory((builded, path) =>
path.node.prefix ?
build(path.node.argument)['='](builded(path.node.argument))[build.raw] :
void (
path.replaceWith(path.node.argument),
path.insertAfter(build(path.node)['='](builded(path.node))[build.raw])
), ["argument"], (path) => path.node.prefix
),
}

这样,我们在访问Expression的节点时不需要关心是不是TypeScript、类型匹配与否等问题,只关心创建的新节点。

  • 标题: Babel插件开发小试:基础Typescript支持
  • 作者: Stone926
  • 创建于 : 2024-12-10 20:34:27
  • 更新于 : 2025-04-17 23:54:59
  • 链接: https://stone926.github.io/2024/12/10/babel-plugin-operator3/
  • 版权声明: 本文为公有领域作品,可自由转载、引用。
目录
Babel插件开发小试:基础Typescript支持