Type-only Migrate by AST

5d9cd19df0e91caac118b793b4f803d5?s=47 Takepepe
March 23, 2020

Type-only Migrate by AST

5d9cd19df0e91caac118b793b4f803d5?s=128

Takepepe

March 23, 2020
Tweet

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. ご静聴ありがとうございました