たとえば、JSDocUnknownType
とか。
TSなのにJSDoc?どういうことやねん!ってなった我々(自分だけ)は、調査のためにアマゾンの奥地へと向かった・・・。
OXCでの出会い
まず最初にこれを見たのは、OXCのTS ASTを眺めていた時。
この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
接頭辞を付けてるみたいだった。中身はほとんどそのままになる。
というわけで、TS本家でパースされたASTも同様の構造になっていて、名前だけが違う。
当然typescript本家にも
なんならもっといっぱい種類が定義されてる。
パーサーのコードはこのあたりから追える。
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
ちなみに、()
を消さないようにするオプションは存在しない!
まぁ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
)以外はすべてパースできないようだった。