Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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'

Slide 22

Slide 22 text

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'

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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'

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

複数 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'

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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