🐷

TS4.5 の type Modifiers on Import Names のパースがおもしろい

4 min read

TypeScript 4.5 のベータ版がアナウンスされました。

https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/

たくさんの新機能があり、大きなバージョンになりそうです。

それらの新機能の中に type Modifiers on Import Names というものがあります。

これは、TypeScript 3.8 から追加された Type-only imports and exports を specifier 単位で指定できるようにしたものです。

今までは、

import { Foo1 } from "foo";
import type { Foo2 } from "foo";

とかいていたものを、これからは

import { Foo1, type Foo2 } from "foo";

と書くことができます。

便利ですね。

パース処理がおもしろい

最近私はこの構文のパーサーを Babel のために書いています。

https://github.com/babel/babel/pull/13802

挙動を TypeScript と一貫させるために、本家(microsoft/TypeScript)のコードを読んでいたのですがその処理がおもしろかったので紹介します。

本家コードはここにあります。

https://github.com/microsoft/TypeScript/blob/fc4f9d83d5939047aa6bb2a43965c6e9bbfbc35b/src/compiler/parser.ts#L7411-L7456

一部抜粋して掲載します。

コード
let isTypeOnly = false;
let propertyName: Identifier | undefined;
let canParseAsKeyword = true;
let name = parseIdentifierName();
if (name.escapedText === "type") {
  // If the first token of an import specifier is 'type', there are a lot of possibilities,
  // especially if we see 'as' afterwards:
  //
  // import { type } from "mod";          - isTypeOnly: false,   name: type
  // import { type as } from "mod";       - isTypeOnly: true,    name: as
  // import { type as as } from "mod";    - isTypeOnly: false,   name: as,    propertyName: type
  // import { type as as as } from "mod"; - isTypeOnly: true,    name: as,    propertyName: as
  if (token() === SyntaxKind.AsKeyword) {
    // { type as ...? }
    const firstAs = parseIdentifierName();
    if (token() === SyntaxKind.AsKeyword) {
      // { type as as ...? }
      const secondAs = parseIdentifierName();
      if (tokenIsIdentifierOrKeyword(token())) {
        // { type as as something }
        isTypeOnly = true;
        propertyName = firstAs;
        name = parseNameWithKeywordCheck();
        canParseAsKeyword = false;
      } else {
        // { type as as }
        propertyName = name;
        name = secondAs;
        canParseAsKeyword = false;
      }
    } else if (tokenIsIdentifierOrKeyword(token())) {
      // { type as something }
      propertyName = name;
      canParseAsKeyword = false;
      name = parseNameWithKeywordCheck();
    } else {
      // { type as }
      isTypeOnly = true;
      name = firstAs;
    }
  } else if (tokenIsIdentifierOrKeyword(token())) {
    // { type something ...? }
    isTypeOnly = true;
    name = parseNameWithKeywordCheck();
  }
}

if (canParseAsKeyword && token() === SyntaxKind.AsKeyword) {
  propertyName = name;
  parseExpected(SyntaxKind.AsKeyword);
  name = parseNameWithKeywordCheck();
}

まあ具体的なところは無視して読み飛ばしてほしいんですが、注目してほしいのはこのコメントです。

// import { type } from "mod";          - isTypeOnly: false,   name: type
// import { type as } from "mod";       - isTypeOnly: true,    name: as
// import { type as as } from "mod";    - isTypeOnly: false,   name: as,    propertyName: type
// import { type as as as } from "mod"; - isTypeOnly: true,    name: as,    propertyName: as

namepropertyName という変数に、as の左右の specifier を代入していくわけですが、そのエッジケースが書かれています。

1:

import { type } from "mod";  

mod というモジュールから type という識別子を named import します。これは type-only ではありません。

2:

import { type as } from "mod";

mod というモジュールから as という識別子を type-only import します。

3:

import { type as as } from "mod";

mod というモジュールから、type という識別子を as という名前で import します。type-only import ではありません。

4:

import { type as as as } from "mod";

mod というモジュールから、as という識別子を as という名前で type-only import します。

おわりに

Babel のためのこのロジックを書きましたが、めんどうくさかったです。

みなさんはこういうコードを書かない方が良いと思います。

Discussion

ログインするとコメントできます