🧊

TypeScriptのASTにおける`JSDocXxxType`というノード

たとえば、JSDocUnknownTypeとか。

TSなのにJSDoc?どういうことやねん!ってなった我々(自分だけ)は、調査のためにアマゾンの奥地へと向かった・・・。

OXCでの出会い

まず最初にこれを見たのは、OXCのTS ASTを眺めていた時。

https://github.com/oxc-project/oxc/blob/d48e8864d32a42664d2aa96bbdcf287828a5904d/crates/oxc_ast/src/ast/ts.rs#L1626-L1654

この3つが定義されてる。

  • JSDocNullableType
  • JSDocNonNullableType
  • JSDocUnknownType

JSDoc接頭辞はついてるけど、いわゆるJSDocコメント内の話ではない。 だってコメントの中身の型情報まではパースしないから。

どうやったらASTとしてご対面できるかというと、

// - TSTypeAliasDeclaration
//   - id: BindingIdentifier
//     - name: "A1"
//   - type_annotation: JSDocNullableType
//     - type_annotation: TSStringKeyword
type A1 = ?string;

// 前にも後にも両方にも付けられる
type A2 = string?;
type A3 = ?string?;

という感じらしい。

いわゆる型を定義している場所で、?!を置くと、それがJSDocXxxTypeになってた。

型じゃない場所での?!、たとえばa!.b?.cみたいなのは、単にTSNonNullExpressionとかoptional(Static)MemberExpressionのチェーンになるだけ。

typescript-eslintにもある

同様のノードが生息しているのを観測できる。

// TSJSDocNullableType
type A1 = string?;
type A2 = ?string;
type A3 = ?string?;

// TSJSDocNonNullableType
type B1 = string!;
type B2 = !string!;
type B3 = !string!;

// TSJSDocUnknownType
type G<T> = T;
type C1 = G<?>;

が、ASTは微妙に違う。

まず、TSJSDocNullableTypeというように、TS接頭辞がつく点。

typescript-eslintでは、ASTがどういう形をしてるかSpecがまとまっていて、リポジトリで探せるようになってる。

https://github.com/typescript-eslint/typescript-eslint/tree/main/packages/ast-spec/src

けど、このTSJSDocXxxTypeシリーズはどこにもいない。

どうやら、typescript-estreeがコードを本家typescriptでASTにパースした後、ESTreeっぽく変換する処理の過程で、このTS接頭辞を付けてるみたいだった。中身はほとんどそのままになる。

https://github.com/typescript-eslint/typescript-eslint/blob/69e2f6c0d371f304c6793ba1801adde10a89372b/packages/typescript-estree/src/convert.ts#L3543

というわけで、TS本家でパースされたASTも同様の構造になっていて、名前だけが違う。

当然typescript本家にも

なんならもっといっぱい種類が定義されてる。

https://github.com/microsoft/TypeScript/blob/83dc0bb2ed91fe0815ab28dc3ff95fae7425e413/src/compiler/types.ts#L402-L404

パーサーのコードはこのあたりから追える。

https://github.com/microsoft/TypeScript/blob/83dc0bb2ed91fe0815ab28dc3ff95fae7425e413/src/compiler/parser.ts#L3847

type A = *;JSDocAllTypeっていうらしいし、type A = ???!!!?!?!?!1!?;みたいなのも、型エラーになってもパースエラーにならないんですって・・。

OptionalType vs JSDocNullableType

閑話休題。

あともう1つ気になってたのは、

// -----------------
// typescript-eslint
// -----------------
// TSTupleType > TSOptionalType 👀 > TSUnionType
type A4 = [(1 | 2)?];
// TSJSDocNullableType > TSUnionType
type A5 = (5 | 6)?;
// TSTupleType > TSOptionalType
type A6 = [1?];

// TSTupleType > TSJSDocNonNullableType > TSUnionType
type B4 = [(3 | 4)!];
// TSJSDocNonNullableType > TSUnionType
type B5 = (3 | 4)!;

というように、タプルなど特定の場面における?は、TSJSDocNullableTypeではなく、TSOptionalTypeになるところ。

ここはTS本体由来の処理結果なので、当然TS本体でもそういうASTが出力されてる。

// ----------
// typescript
// ----------
// TupleType > OptionalType 👀 > ParenthesizedType > UnionType
type A4 = [(1 | 2)?];
// JSDocNullableType > ParenthesizedType > UnionType
type A5 = (1 | 2)?;
// TupleType > OptionalType
type A6 = [1?];

// TupleType > JSDocNonNullableType > ParenthesizedType > UnionType
type B4 = [(3 | 4)!];
// JSDocNonNullableType > ParenthesizedType > UnionType
type B5 = (3 | 4)!;

TS本体では、ParenthesizedTypeも出力される。

typescript-estreeでは、ESTreeを名乗る以上ParethesizedExpressionと同様に、ParenthesizedTypeは存在を抹消されるってことらしい。

https://github.com/typescript-eslint/typescript-eslint/blob/69e2f6c0d371f304c6793ba1801adde10a89372b/packages/typescript-estree/src/convert.ts#L2891 https://github.com/typescript-eslint/typescript-eslint/blob/69e2f6c0d371f304c6793ba1801adde10a89372b/packages/typescript-estree/src/convert.ts#L3338

ちなみに、()を消さないようにするオプションは存在しない!

https://github.com/typescript-eslint/typescript-eslint/blob/69e2f6c0d371f304c6793ba1801adde10a89372b/packages/types/src/parser-options.ts#L75

まぁJSと違って@typeキャストの出番はないはずやし、困ることもないのであろう。

まとめると、

// typescript-eslint(typescriptも同様)
type X = [
  1?,  // TSOptionalType
  ?1,  // TSJSDocNullableType
  ?1?, // TSJSDocNullableType > TSJSDocNullableType
  ?1!, // TSJSDocNullableType > TSJSDocNonNullableType
  1!,  // TSJSDocNonNullableType
  !1,  // TSJSDocNonNullableType
  !1!, // TSJSDocNonNullableType > TSJSDocNonNullableType
  !1?, // TSOptionalType > TSJSDocNonNullableType
];

type X1 = 1?;  // TSJSDocNullableType
type X2 = ?1;  // TSJSDocNullableType
type X3 = ?1?; // TSJSDocNullableType > TSJSDocNullableType
type X4 = ?1!; // TSJSDocNullableType > TSJSDocNonNullableType
type X5 = 1!;  // TSJSDocNonNullableType
type X6 = !1;  // TSJSDocNonNullableType
type X7 = !1!; // TSJSDocNonNullableType > TSJSDocNonNullableType
type X8 = !1?; // TSJSDocNullableType > TSJSDocNonNullableType

タプルの中では?JSDocNullableTypeではなくOptionalTypeとして扱うってことかな? タプル以外にもこういうケースはあるんだろうか・・・。

ちなみに、BabelもSWCもBiomeも、[string?]TSOptionalType)以外はすべてパースできないようだった。