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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  15. 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 full-size slide

  16. 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 full-size slide

  17. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  27. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  30. 複数 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 full-size slide

  31. 出力結果のソート制御は現状いれていませんが、
    この辺りの設定も出来ると、コードの掃除にも使えそうです。
    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 full-size slide

  32. コードジェネレーター・型を利用したチェッカーなど、
    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 full-size slide

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

    View full-size slide