如何通过TypeScript API提取无as const时JSX属性的窄化类型
我正在尝试开发一款可记录文件内所有
JSX属性及其对应窄化TypeScript类型的文档生成软件。
输入文件示例:
div标签的test属性被定义为仅接受值为hello的字符串字面量类型
const testAsConst = 'hello' as const; const test = 'hello'; // 上述两个变量传入属性均可正常通过TS类型校验 <div test={test} />;
预期输出结果:[ [ 'test', '"hello"' ] ]
变量未添加as const修饰时,程序输出为[ [ 'test', 'string' ] ]
变量添加as const修饰时,程序输出为[ [ 'test', '"hello"' ] ],符合预期
我清楚
as const的作用逻辑,但上述示例中未添加as const时TypeScript也不会抛出类型错误,说明此时test变量传入属性时的类型应当被窄化为"hello"而非宽泛的string类型,希望能实现无as const时也能提取到正确的窄化类型。
当前实现功能的软件源码
import typescript from 'typescript'; const program = typescript.createProgram(['inputFile.tsx'], { target: typescript.ScriptTarget.ESNext }); const typeChecker = program.getTypeChecker(); for (const sourceFile of program.getSourceFiles()) { if (!sourceFile.isDeclarationFile) { const output = visit(sourceFile); console.log(output); } } function visit(node: typescript.Node, output2: [string, string][] = []): [string, string][] | undefined { if (typescript.isJsxAttribute(node)) { const attributeName = node.name.text; const symbol = typeChecker.getSymbolAtLocation(node.name); if (symbol) { const type = typeChecker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!); output2.push([attributeName, typeChecker.typeToString(type)]); } } typescript.forEachChild(node, node2 => { visit(node2, output2); }); return output2; }
问题根因
当前代码取类型的逻辑存在错误:现有写法获取的是JSX属性名对应属性定义处的声明类型,并非传入属性值在当前JSX上下文里的窄化类型。
按照TS的类型推断规则,未加as const的const test = 'hello'声明,变量本身的默认推断类型是拓宽(widened)后的string;只有当这个变量被传入要求更窄字面量类型的位置时,TS才会在传值位置做上下文类型窄化,校验是否匹配目标类型。原有写法完全没有获取传值位置的类型,自然只能拿到宽类型string。
修复方案
不要通过属性名的symbol拿声明类型,直接读取JSX属性的值节点,在当前JSX上下文位置调用类型检查器获取类型,就能拿到经过上下文窄化后的字面量类型。
首先修正Program创建配置,对齐TS常规校验行为:
const program = typescript.createProgram(['inputFile.tsx'], { target: typescript.ScriptTarget.ESNext, strict: true, jsx: typescript.JsxEmit.React });
再修改核心visit逻辑:
function visit(node: typescript.Node, output2: [string, string][] = []): [string, string][] | undefined { if (typescript.isJsxAttribute(node)) { const attributeName = node.name.text; if (!node.initializer) return; // 区分表达式传值和字面量传值场景,拿到实际的属性值节点 const valueNode = typescript.isJsxExpression(node.initializer) ? node.initializer.expression : node.initializer; if (!valueNode) return; // 直接在值节点位置取类型,自动应用上下文窄化逻辑 const type = typeChecker.getTypeAtLocation(valueNode); output2.push([attributeName, typeChecker.typeToString(type)]); } typescript.forEachChild(node, node2 => { visit(node2, output2); }); return output2; }
修改后,即使变量未加as const,只要传值位置能匹配目标字面量类型,就能正确拿到"hello"的窄化类型结果,和TS实际类型校验行为完全一致。
内容的提问来源于stack exchange,提问作者Altaula

