Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. View Slide

  2. View Slide

  3. View Slide

  4. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. コードが膨大な場合、この移行は骨が折れる作業です。
    今日は、この移行をサポートするツールを
    TypeScript Compiler API で作った話をします。
    import { A, C } from 'module'
    import type { B } from 'module'
    手動以外、方法はない…?

    View Slide

  9. https://github.com/takefumi-yoshii/type-only-imports-converter

    View Slide

  10. View Slide

  11. TypeScript Compiler API を使うことで、
    TypeScript AST を簡単に取り扱う事が出来ます。
    SourceFile
    ImportDeclaration
    ImportClause
    NamedImports
    ImportSpecifier
    Identifier
    ImportSpecifier
    Identifier
    ImportSpecifier
    Identifier
    StringLiteral
    EndOfFileToken
    import { A, B, C } from 'module'

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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'

    View Slide

  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'

    View Slide

  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'

    View Slide

  23. View Slide

  24. AST組み替えにあたり「型定義なのか?ランタイム実装なのか?」
    を判別する必要があります。
    import { A, B, C } from 'module'
    ? ? ?

    View Slide

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

    View Slide

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

    View Slide

  27. ts.TypeChecker がそのインスタンスです。
    checker に生えている、2つの API を利用します。
    const symbol = checker.getSymbolAtLocation(node)
    const aliasedSymbol = checker.getAliasedSymbol(symbol)

    View Slide

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

    View Slide

  29. checker.getAliasedSymbol を利用すると、
    参照元となる外部ファイルの ts.Symbol を取得できます。
    const symbol = checker.getSymbolAtLocation(node)
    const aliasedSymbol = checker.getAliasedSymbol(symbol)

    View Slide

  30. aliasedSymbol.flags === ts.SymbolFlags.Interface ||
    aliasedSymbol.flags === ts.SymbolFlags.TypeAlias
    最後に ts.SymbolFlags で定められた enum 値と評価し判別。
    CompilerAPI には、この様に判別する enum がいくつか定義されています。

    View Slide

  31. View Slide

  32. TypeScript Compiler API は、
    ドキュメントが少ないものの、
    実体は陳腐化することなく
    保守されています。
    "Type-only" に関するAST情報と、
    それを扱うAPIも追加されています。
    https://github.com/microsoft/TypeScript/pull/35200

    View Slide

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

    View Slide

  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'

    View Slide

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

    View Slide

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

    View Slide

  37. View Slide

  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'

    View Slide

  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';

    View Slide

  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';

    View Slide

  41. ご静聴ありがとうございました

    View Slide