Open4

TypeCheckerをいじっていて詰まったところメモ

倉敷倉敷

最近はtypescript向けeslintプラグインを作る目的でTypeCheckerをこねこねしています。この辺の日本語情報は全然見つからなかったので、何かの役に立つかもの思いを込めてメモを残しておきます。

// eslintプラグイン作成環境で、typecheckerインスタンスを作成
import { ESLintUtils } from "@typescript-eslint/utils";
const createRule = ESLintUtils.RuleCreator((name) => `https://npmjs.com/package/${name}`);
createRule({
  create(context) {
    const parserServices = context.sourceCode.parserServices;
    const checker = parserServices?.program?.getTypeChecker() // これ
  }
  // 省略
})
// typechecker の型定義インポート
import { TypeChecker } from "typescript";

格闘した結果はだいたいここにあります

https://github.com/tfs-tada/eslint-plugin-no-excess-property

倉敷倉敷

type Type = [] の扱い

[] は neverやanyの配列ではなく長さゼロのタプルなので、 checker.isArrayType が trueになりません。検出するときは、checker.isTupleType などを使う必要あります。なお、checker.isArrayLikeType を使うと配列だろうがタプルだろうが両方検知できます。

複雑な型(大掛かりなユニオンなど)を解体すると type SampleArray = {name: age}[] | [] みたいな型が現れることがありますが、あれは配列とタプルのユニオンなんですね。

ちなみに、 isArrayTypeisArrayLikeTypeisTupleType がtrueだったとしても、型に is のガードは効きません。githubを見た限りだと、これを嫌って自前の型ガードをつけている方がちらほらいそうです。

倉敷倉敷

jsxのattributes

TypeScriptのASTでjsxのattributeを参照する場合、おそらく JSXOpeningElement を確認し、 node.attributes から attributeの配列を確認することになると思われます。

const Component = (props: { name: string; age: number }) => <></>;
const taro = { name: "taro", age: 10 };
const app = <Component {...taro} name="jiro" />;

このとき node.attributes には、 [taroの情報が含まれたspread表現のattribute , name="jiro"の情報が含まれたattribute] の2つが含まれます。

ただし、これを扱う上で2つ課題があり、いくらか頭を悩ませることになりました。

  • attribute の typeにはJSXAttributeJSXSpreadAttribute の2種類があり、情報形式が結構違う
    • 型定義上、JSXOpeningElement.attributes: (TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute)[] となっている
  • 最終的にjsxに渡される型を取得する方法は(おそらく)存在しない
    • 上の例の場合、 Componentに最終的に渡されるのは {name:"jiro", age: 10} のオブジェクト型ですが、これをTypeCheckerで取得することはできなさそうです

無理やり型情報をまとめようとしたら以下のようになりました。各attributeのts.Type情報を取得し、プロパティ名をkeyにしてマージしていきます。

本当はts.Type型でobjectをまとめたかったのですが、自分にはRecord型にするのが精一杯でした。うまい方法をご存知の方いましたら教えてください。

const allProps: Record<string, ts.Type> = {};
node.attributes.forEach((attribute) => {
  if (attribute.type === "JSXSpreadAttribute") {
    const attrNode = parserServices.esTreeNodeToTSNodeMap.get(
      attribute.argument,
    );
    const attrType = checker.getTypeAtLocation(attrNode);
    attrType.getProperties().forEach((prop) => {
      allProps[prop.name] = checker.getTypeOfSymbolAtLocation(
        prop,
        attrNode,
      );
    });
  } else if (attribute.type === "JSXAttribute") {
    const attrName = attribute.name.name;
    if (typeof attrName !== "string") return;
    const attrNode =
      parserServices.esTreeNodeToTSNodeMap!.get(attribute);
    const attrType = checker.getTypeAtLocation(attrNode);
    allProps[attrName] = attrType;
  }
});
倉敷倉敷

TypeCheckerの型定義が足りない

ts.TypeCheckerには数多くのメソッドが定義されていますが、それでも実際に使えるはずのcheckerメソッドが全て使えるわけではなさそうです。実際に使えるcheckerメソッドは、ts.Typeをconsole等で出力すると確認できます。

定義されていないメソッドを使うには、やむを得ない選択ではありますが、自分で拡張する必要がありそうです。

export type Checker = ts.TypeChecker & {
  getElementTypeOfArrayType: (type: ts.Type) => ts.Type | undefined;
  getPromisedTypeOfPromise: (type: ts.Type) => ts.Type | undefined;
};

ちなみに、シンプルなオブジェクトをconsoleで見てみると、170近いメソッドを確認できます

ts.Typeをconsole.logで見た時のcheckerオブジェクト
TypeObject {
    checker: {
      getNodeCount: [Function: getNodeCount],
      getIdentifierCount: [Function: getIdentifierCount],
      getSymbolCount: [Function: getSymbolCount],
      getTypeCount: [Function: getTypeCount],
      getInstantiationCount: [Function: getInstantiationCount],
      getRelationCacheSizes: [Function: getRelationCacheSizes],
      isUndefinedSymbol: [Function: isUndefinedSymbol],
      isArgumentsSymbol: [Function: isArgumentsSymbol],
      isUnknownSymbol: [Function: isUnknownSymbol],
      getMergedSymbol: [Function: getMergedSymbol],
      getDiagnostics: [Function: getDiagnostics2],
      getGlobalDiagnostics: [Function: getGlobalDiagnostics],
      getRecursionIdentity: [Function: getRecursionIdentity],
      getUnmatchedProperties: [GeneratorFunction: getUnmatchedProperties],
      getTypeOfSymbolAtLocation: [Function: getTypeOfSymbolAtLocation],
      getTypeOfSymbol: [Function: getTypeOfSymbol],
      getSymbolsOfParameterPropertyDeclaration: [Function: getSymbolsOfParameterPropertyDeclaration],
      getDeclaredTypeOfSymbol: [Function: getDeclaredTypeOfSymbol],
      getPropertiesOfType: [Function: getPropertiesOfType],
      getPropertyOfType: [Function: getPropertyOfType],
      getPrivateIdentifierPropertyOfType: [Function: getPrivateIdentifierPropertyOfType],
      getTypeOfPropertyOfType: [Function: getTypeOfPropertyOfType],
      getIndexInfoOfType: [Function: getIndexInfoOfType],
      getIndexInfosOfType: [Function: getIndexInfosOfType],
      getIndexInfosOfIndexSymbol: [Function: getIndexInfosOfIndexSymbol],
      getSignaturesOfType: [Function: getSignaturesOfType],
      getIndexTypeOfType: [Function: getIndexTypeOfType],
      getIndexType: [Function: getIndexType],
      getBaseTypes: [Function: getBaseTypes],
      getBaseTypeOfLiteralType: [Function: getBaseTypeOfLiteralType],
      getWidenedType: [Function: getWidenedType],
      getTypeFromTypeNode: [Function: getTypeFromTypeNode],
      getParameterType: [Function: getTypeAtPosition],
      getParameterIdentifierInfoAtPosition: [Function: getParameterIdentifierInfoAtPosition],
      getPromisedTypeOfPromise: [Function: getPromisedTypeOfPromise],
      getAwaitedType: [Function: getAwaitedType],
      getReturnTypeOfSignature: [Function: getReturnTypeOfSignature],
      isNullableType: [Function: isNullableType],
      getNullableType: [Function: getNullableType],
      getNonNullableType: [Function: getNonNullableType],
      getNonOptionalType: [Function: removeOptionalTypeMarker],
      getTypeArguments: [Function: getTypeArguments],
      typeToTypeNode: [Function: typeToTypeNode],
      indexInfoToIndexSignatureDeclaration: [Function: indexInfoToIndexSignatureDeclaration],
      signatureToSignatureDeclaration: [Function: signatureToSignatureDeclaration],
      symbolToEntityName: [Function: symbolToEntityName],
      symbolToExpression: [Function: symbolToExpression],
      symbolToNode: [Function: symbolToNode],
      symbolToTypeParameterDeclarations: [Function: symbolToTypeParameterDeclarations],
      symbolToParameterDeclaration: [Function: symbolToParameterDeclaration],
      typeParameterToDeclaration: [Function: typeParameterToDeclaration],
      getSymbolsInScope: [Function: getSymbolsInScope],
      getSymbolAtLocation: [Function: getSymbolAtLocation],
      getIndexInfosAtLocation: [Function: getIndexInfosAtLocation],
      getShorthandAssignmentValueSymbol: [Function: getShorthandAssignmentValueSymbol],
      getExportSpecifierLocalTargetSymbol: [Function: getExportSpecifierLocalTargetSymbol],
      getExportSymbolOfSymbol: [Function: getExportSymbolOfSymbol],
      getTypeAtLocation: [Function: getTypeAtLocation],
      getTypeOfAssignmentPattern: [Function: getTypeOfAssignmentPattern],
      getPropertySymbolOfDestructuringAssignment: [Function: getPropertySymbolOfDestructuringAssignment],
      signatureToString: [Function: signatureToString],
      typeToString: [Function: typeToString],
      symbolToString: [Function: symbolToString],
      typePredicateToString: [Function: typePredicateToString],
      writeSignature: [Function: writeSignature],
      writeType: [Function: writeType],
      writeSymbol: [Function: writeSymbol],
      writeTypePredicate: [Function: writeTypePredicate],
      getAugmentedPropertiesOfType: [Function: getAugmentedPropertiesOfType],
      getRootSymbols: [Function: getRootSymbols],
      getSymbolOfExpando: [Function: getSymbolOfExpando],
      getContextualType: [Function: getContextualType],
      getContextualTypeForObjectLiteralElement: [Function: getContextualTypeForObjectLiteralElement],
      getContextualTypeForArgumentAtIndex: [Function: getContextualTypeForArgumentAtIndex],
      getContextualTypeForJsxAttribute: [Function: getContextualTypeForJsxAttribute],
      isContextSensitive: [Function: isContextSensitive],
      getTypeOfPropertyOfContextualType: [Function: getTypeOfPropertyOfContextualType],
      getFullyQualifiedName: [Function: getFullyQualifiedName],
      getResolvedSignature: [Function: getResolvedSignature],
      getCandidateSignaturesForStringLiteralCompletions: [Function: getCandidateSignaturesForStringLiteralCompletions],
      getResolvedSignatureForSignatureHelp: [Function: getResolvedSignatureForSignatureHelp],
      getExpandedParameters: [Function: getExpandedParameters],
      hasEffectiveRestParameter: [Function: hasEffectiveRestParameter],
      containsArgumentsReference: [Function: containsArgumentsReference],
      getConstantValue: [Function: getConstantValue],
      isValidPropertyAccess: [Function: isValidPropertyAccess],
      isValidPropertyAccessForCompletions: [Function: isValidPropertyAccessForCompletions],
      getSignatureFromDeclaration: [Function: getSignatureFromDeclaration],
      isImplementationOfOverload: [Function: isImplementationOfOverload],
      getImmediateAliasedSymbol: [Function: getImmediateAliasedSymbol],
      getAliasedSymbol: [Function: resolveAlias],
      getEmitResolver: [Function: getEmitResolver],
      getExportsOfModule: [Function: getExportsOfModuleAsArray],
      getExportsAndPropertiesOfModule: [Function: getExportsAndPropertiesOfModule],
      forEachExportAndPropertyOfModule: [Function: forEachExportAndPropertyOfModule],
      getSymbolWalker: [Function: getSymbolWalker],
      getAmbientModules: [Function: getAmbientModules],
      getJsxIntrinsicTagNamesAt: [Function: getJsxIntrinsicTagNamesAt],
      isOptionalParameter: [Function: isOptionalParameter],
      tryGetMemberInModuleExports: [Function: tryGetMemberInModuleExports],
      tryGetMemberInModuleExportsAndProperties: [Function: tryGetMemberInModuleExportsAndProperties],
      tryFindAmbientModule: [Function: tryFindAmbientModule],
      tryFindAmbientModuleWithoutAugmentations: [Function: tryFindAmbientModuleWithoutAugmentations],
      getApparentType: [Function: getApparentType],
      getUnionType: [Function: getUnionType],
      isTypeAssignableTo: [Function: isTypeAssignableTo],
      createAnonymousType: [Function: createAnonymousType],
      createSignature: [Function: createSignature],
      createSymbol: [Function: createSymbol],
      createIndexInfo: [Function: createIndexInfo],
      getAnyType: [Function: getAnyType],
      getStringType: [Function: getStringType],
      getStringLiteralType: [Function: getStringLiteralType],
      getNumberType: [Function: getNumberType],
      getNumberLiteralType: [Function: getNumberLiteralType],
      getBigIntType: [Function: getBigIntType],
      createPromiseType: [Function: createPromiseType],
      createArrayType: [Function: createArrayType],
      getElementTypeOfArrayType: [Function: getElementTypeOfArrayType],
      getBooleanType: [Function: getBooleanType],
      getFalseType: [Function: getFalseType],
      getTrueType: [Function: getTrueType],
      getVoidType: [Function: getVoidType],
      getUndefinedType: [Function: getUndefinedType],
      getNullType: [Function: getNullType],
      getESSymbolType: [Function: getESSymbolType],
      getNeverType: [Function: getNeverType],
      getOptionalType: [Function: getOptionalType],
      getPromiseType: [Function: getPromiseType],
      getPromiseLikeType: [Function: getPromiseLikeType],
      getAsyncIterableType: [Function: getAsyncIterableType],
      isSymbolAccessible: [Function: isSymbolAccessible],
      isArrayType: [Function: isArrayType],
      isTupleType: [Function: isTupleType],
      isArrayLikeType: [Function: isArrayLikeType],
      isEmptyAnonymousObjectType: [Function: isEmptyAnonymousObjectType],
      isTypeInvalidDueToUnionDiscriminant: [Function: isTypeInvalidDueToUnionDiscriminant],
      getExactOptionalProperties: [Function: getExactOptionalProperties],
      getAllPossiblePropertiesOfTypes: [Function: getAllPossiblePropertiesOfTypes],
      getSuggestedSymbolForNonexistentProperty: [Function: getSuggestedSymbolForNonexistentProperty],
      getSuggestionForNonexistentProperty: [Function: getSuggestionForNonexistentProperty],
      getSuggestedSymbolForNonexistentJSXAttribute: [Function: getSuggestedSymbolForNonexistentJSXAttribute],
      getSuggestedSymbolForNonexistentSymbol: [Function: getSuggestedSymbolForNonexistentSymbol],
      getSuggestionForNonexistentSymbol: [Function: getSuggestionForNonexistentSymbol],
      getSuggestedSymbolForNonexistentModule: [Function: getSuggestedSymbolForNonexistentModule],
      getSuggestionForNonexistentExport: [Function: getSuggestionForNonexistentExport],
      getSuggestedSymbolForNonexistentClassMember: [Function: getSuggestedSymbolForNonexistentClassMember],
      getBaseConstraintOfType: [Function: getBaseConstraintOfType],
      getDefaultFromTypeParameter: [Function: getDefaultFromTypeParameter],
      resolveName: [Function: resolveName],
      getJsxNamespace: [Function: getJsxNamespace],
      getJsxFragmentFactory: [Function: getJsxFragmentFactory],
      getAccessibleSymbolChain: [Function: getAccessibleSymbolChain],
      getTypePredicateOfSignature: [Function: getTypePredicateOfSignature],
      resolveExternalModuleName: [Function: resolveExternalModuleName],
      resolveExternalModuleSymbol: [Function: resolveExternalModuleSymbol],
      tryGetThisTypeAt: [Function: tryGetThisTypeAt],
      getTypeArgumentConstraint: [Function: getTypeArgumentConstraint],
      getSuggestionDiagnostics: [Function: getSuggestionDiagnostics],
      runWithCancellationToken: [Function: runWithCancellationToken],
      getLocalTypeParametersOfClassOrInterfaceOrTypeAlias: [Function: getLocalTypeParametersOfClassOrInterfaceOrTypeAlias],
      isDeclarationVisible: [Function: isDeclarationVisible],
      isPropertyAccessible: [Function: isPropertyAccessible],
      getTypeOnlyAliasDeclaration: [Function: getTypeOnlyAliasDeclaration],
      getMemberOverrideModifierStatus: [Function: getMemberOverrideModifierStatus],
      isTypeParameterPossiblyReferenced: [Function: isTypeParameterPossiblyReferenced],
      typeHasCallOrConstructSignatures: [Function: typeHasCallOrConstructSignatures]
  }
},

これに対し、typescript 5.4.5 の ts.TypeCheckerに定義されたメソッドは85個だけでした。結構足りないです。どうして足りないのかはちょっと分かりません。有識者さんいましたら教えてください。