$30 off During Our Annual Pro Sale. View Details »

Type-only Migrate by AST

Takepepe
March 23, 2020

Type-only Migrate by AST

Takepepe

March 23, 2020
Tweet

More Decks by Takepepe

Other Decks in Technology

Transcript

  1. None
  2. None
  3. None
  4. None
  5. TypeScript 3.8で「型定義のみ」を明示的に読み込む構文が追加されました。 import type { TypeAlias } from '...' の様に利用します。(Flowには従来あったもの)

  6. これまで、値・型定義を区別せず import していたコード。 明示的にする目的で、Type-only を利用していくこともあるでしょう。 そのためには、ひとつひとつ「手動・目視」で調べなければいけません。 import { A, B,

    C } from 'module'
  7. 複数の定義を、1行で読み込んでいる場所では 「型定義なのか?ランタイム実装なのか?」 即座に区別がつきません。命名規則でこれを補うこともありました。 import { A, B, C } from

    'module' 値 型 Class
  8. コードが膨大な場合、この移行は骨が折れる作業です。 今日は、この移行をサポートするツールを TypeScript Compiler API で作った話をします。 import { A, C

    } from 'module' import type { B } from 'module' 手動以外、方法はない…?
  9. https://github.com/takefumi-yoshii/type-only-imports-converter

  10. None
  11. TypeScript Compiler API を使うことで、 TypeScript AST を簡単に取り扱う事が出来ます。 SourceFile ImportDeclaration ImportClause

    NamedImports ImportSpecifier Identifier ImportSpecifier Identifier ImportSpecifier Identifier StringLiteral EndOfFileToken import { A, B, C } from 'module'
  12. この様なSRCコードがあった場合。 トランスパイル前に、AST に変換されます。 SourceFile ImportDeclaration ImportClause NamedImports ImportSpecifier Identifier ImportSpecifier

    Identifier ImportSpecifier Identifier StringLiteral EndOfFileToken import { A, B, C } from 'module'
  13. AST はこの様になります。 構文の意味単位で、区切られています。 SourceFile ImportDeclaration ImportClause NamedImports ImportSpecifier Identifier ImportSpecifier

    Identifier ImportSpecifier Identifier StringLiteral EndOfFileToken import { A, B, C } from 'module'
  14. ファイルは、ts.SourceFile で表され… SourceFile ImportDeclaration ImportClause NamedImports ImportSpecifier Identifier ImportSpecifier Identifier

    ImportSpecifier Identifier StringLiteral EndOfFileToken import { A, B, C } from 'module'
  15. import 宣言は、ts.ImportDeclaration で表され… SourceFile ImportDeclaration ImportClause NamedImports ImportSpecifier Identifier ImportSpecifier

    Identifier ImportSpecifier Identifier StringLiteral EndOfFileToken import { A, B, C } from 'module'
  16. import 句は、ts.ImportClause で表され… SourceFile ImportDeclaration ImportClause NamedImports ImportSpecifier Identifier ImportSpecifier

    Identifier ImportSpecifier Identifier StringLiteral EndOfFileToken import { A, B, C } from 'module'
  17. import 指定子は、ts.ImportSpecifier で表されます。 SourceFile ImportDeclaration ImportClause NamedImports ImportSpecifier Identifier ImportSpecifier

    Identifier ImportSpecifier Identifier StringLiteral EndOfFileToken import { A, B, C } from 'module'
  18. すべての Node は、ts.Node のサブセットです。 SourceFile ImportDeclaration ImportClause NamedImports ImportSpecifier Identifier

    ImportSpecifier Identifier ImportSpecifier Identifier StringLiteral EndOfFileToken import { A, B, C } from 'module'
  19. この AST は組み替えることが可能です。 SourceFile ImportDeclaration ImportClause NamedImports ImportSpecifier Identifier ImportSpecifier

    Identifier StringLiteral EndOfFileToken import { A, B } from 'module'
  20. SourceFile ImportDeclaration ImportClause NamedImports ImportSpecifier Identifier StringLiteral ImportDeclaration ImportClause NamedImports

    ImportSpecifier Identifier StringLiteral EndOfFileToken 元のSRCコードから、新しいSRCコードを得ることができます。 import { A } from 'module' import { B } from 'module'
  21. SourceFile ImportDeclaration ImportClause NamedImports ImportSpecifier Identifier StringLiteral ImportDeclaration ImportClause NamedImports

    ImportSpecifier Identifier StringLiteral EndOfFileToken それは、Node.js で JSON を組み替える作業と似ています。 import { A } from 'module' import { B } from 'module'
  22. SourceFile ImportDeclaration ImportClause NamedImports ImportSpecifier Identifier StringLiteral ImportDeclaration ImportClause NamedImports

    ImportSpecifier Identifier StringLiteral EndOfFileToken 型定義・ランタイム実装を「仕分け」し、"Type-only" import に変換します。 import { A } from 'module' import type { B } from 'module'
  23. None
  24. AST組み替えにあたり「型定義なのか?ランタイム実装なのか?」 を判別する必要があります。 import { A, B, C } from 'module'

    ? ? ?
  25. 今回作成したサンプルツールでは、 「確実に型定義である」 Interface・Type Alias を 選り分けるものとしました。 import { A, B,

    C } from 'module' ? ? ?
  26. TypeScript Compiler API を利用すれば、 その Node の内訳を把握する事ができます。 import { A,

    B, C } from 'module' 値 型 Class
  27. ts.TypeChecker がそのインスタンスです。 checker に生えている、2つの API を利用します。 const symbol = checker.getSymbolAtLocation(node)

    const aliasedSymbol = checker.getAliasedSymbol(symbol)
  28. const symbol = checker.getSymbolAtLocation(node) const aliasedSymbol = checker.getAliasedSymbol(symbol) checker.getSymbolAtLocation を用いて、

    単ファイルスコープの参照元となる ts.Symbol を取得。 これだけでは、import した定義の内訳を確認することができません。
  29. checker.getAliasedSymbol を利用すると、 参照元となる外部ファイルの ts.Symbol を取得できます。 const symbol = checker.getSymbolAtLocation(node) const

    aliasedSymbol = checker.getAliasedSymbol(symbol)
  30. aliasedSymbol.flags === ts.SymbolFlags.Interface || aliasedSymbol.flags === ts.SymbolFlags.TypeAlias 最後に ts.SymbolFlags で定められた

    enum 値と評価し判別。 CompilerAPI には、この様に判別する enum がいくつか定義されています。
  31. None
  32. TypeScript Compiler API は、 ドキュメントが少ないものの、 実体は陳腐化することなく 保守されています。 "Type-only" に関するAST情報と、 それを扱うAPIも追加されています。

    https://github.com/microsoft/TypeScript/pull/35200
  33. ts.ImportClause Node を確認すると、そこには 「isTypeOnly」というプロパティが追加されていることを確認できます。

  34. ts.ImportClause Node を出力するためのASTファクトリー関数。 第4引数は、only type を指定する bool値です。 ts.createImportClause( undefined, ts.createNamedImports([ts.createImportSpecifier(

    undefined, ts.createIdentifier("A") )]), false // <- here ) import { A } from 'module'
  35. これだけで、出力結果に type が付与された、 SRCコードを出力することが可能になります。 ts.createImportClause( undefined, ts.createNamedImports([ts.createImportSpecifier( undefined, ts.createIdentifier("B") )]),

    true // <- here ) import type { B } from 'module'
  36. 他にも、ts.isTypeOnlyImportOrExportDeclaration 関数などが 追加されています。 https://ts-ast-viewer.com/ では、 新しい TypeScript バージョンが出ると即座に追従されるため、 その構文がどの様に AST で表現されるのか、確認することができます。

  37. None
  38. 複数 import 文を列挙していることもあるでしょう。(同じモジュールなのに) 現状のサンプルコードでは、この様なバラバラのコードも マージする様にもなっています。 import type { TypeAlias }

    from './a' import { Interface } from './b' import { Const } from './a' import { TypeAlias as TYPEALIAS } from './a' import { Let, Interface as INTERFACE } from './b'
  39. 出力結果のソート制御は現状いれていませんが、 この辺りの設定も出来ると、コードの掃除にも使えそうです。 import { Const } from './a'; import {

    Let } from './b'; import type { TypeAlias, TypeAlias as TYPEALIAS } from './a'; import type { Interface, Interface as INTERFACE } from './b';
  40. コードジェネレーター・型を利用したチェッカーなど、 Compiler API なら可能なアイディアはまだまだあります。 ぜひ遊んでみてください。 import { Const } from

    './a'; import { Let } from './b'; import type { TypeAlias, TypeAlias as TYPEALIAS } from './a'; import type { Interface, Interface as INTERFACE } from './b';
  41. ご静聴ありがとうございました