コンテンツにスキップ

[TypeScript] 3.0

TypeScript3.0のリリース内容まとめ

Project References

HAD BETTER NORMAL

あるTypeScriptプロジェクトから別のTypeScriptプロジェクトを参照できるようになった。
巨大プロジェクトを分割することができ、以下の様なメリットがある。

  • 結合度が低く部品化された設計ができる
  • 一方向の可視性を定義できる (実装コードからテストコードを見えないようにする etc)
  • 必要な箇所だけビルド(型チェック/トランスパイル)するためビルド速度が上がる

詳細は以下を参照。

ビルドモード

TypeScript3.0ではプロジェクト単位でインクリメンタルビルドができるようになった。
tsc --buildまたはtsc -bとフラグを指定するだけ。

以下のようなプロジェクトがある場合を考える。

  src
├──   index.ts
├──   tsconfig.json
└──   util.ts

従来はtscコマンドを何度実行しても、都度フルビルドをしていた。
ビルドモードを使って必要な時のみビルドさせる。

1度目の実行

1度目の実行では全てのtsファイルがビルドされる。

$ npx tsc -b -v
[20:56:16] Projects in this build:
    * tsconfig.json

[20:56:16] Project 'tsconfig.json' is out of date because output file 'index.js' does not exist

[20:56:16] Building project 'C:/Users/syoum/work/sandbox/typescript/project-reference/src/tsconfig.json'...

index.jsが存在しない時点で、このプロジェクトはリビルドが必要と判断されたからだ。
もちろんutil.jsが無い場合も同じ。

2度目の実行

2度目の実行では実際のビルド処理はスキップされる。

$ npx tsc -b -v
[20:58:44] Projects in this build:
    * tsconfig.json

[20:58:44] Project 'tsconfig.json' is up to date because newest input 'index.ts' is older than oldest output 'util.js'

以下の関係にあるため、ビルドは必要ないと判断するからだ。

新しい

↑
最も古い成果物の`util.js`
最も古い入力ファイルである`index.ts`
↓

古い

ビルドモードの問題点

tscビルドするプロジェクトが別プロジェクトのソースを参照する場合、リビルド判定が正しく行われない。
たとえば以下の構成を考える。

  src
├──   index.ts
├──   tsconfig.json
└──   util.ts
  test  <============== test配下で tsc -b する
├──   index.ts  <====== ../src/util.tsを参照している
└──   tsconfig.json

tsc -bでビルドすると、test/index.jsおよび参照先のsrc/util.jsが生成される。

  src
├──   index.ts
├──   tsconfig.json
├──   util.js  <===== 生成
└──   util.ts
  test
├──   index.js  <==== 生成
├──   index.ts  <==== ../src/util.tsを参照している
└──   tsconfig.json

この状態でutil.tsに変更を加えてもう一度tsc -bでビルドする。
期待するのはsrc/util.jsの再生成だが、test配下には変更がないためビルドは不要と判断される。

  src
├──   index.ts
├──   tsconfig.json
├──   util.js  <===== ❹ リビルドされない(これはまずい)
└──   util.ts  <===== ❸ 変更を加えたのに。。
  test
├──   index.js  <==== ❷ ビルドされない(これはOK)
├──   index.ts  <==== ❶ 変更されていないため..
└──   tsconfig.json

これを解決するには必ず以下の順で処理をしなければいけない。

  1. src配下でtsc -b
  2. test配下でtsc -b

プロジェクト参照を使うと、2の手順のみでOKになる。

プロジェクト参照

参照されるプロジェクト、参照するプロジェクトでそれぞれ設定が必要。

参照されるプロジェクトの設定

ここではsrc/tsconfig.jsonのこと。
comoposite: trueを追加する。

  {
    "compilerOptions": {
      "target": "es5",
      "module": "commonjs",
      "strict": true,
-     "esModuleInterop": true
+     "esModuleInterop": true,
+     "composite": true
    }
  }

これには以下のような効果もある。

  • 設定なしに別プロジェクトの参照を禁止する (test配下のファイルをimportできない)
  • ビルド時に.d.tsファイルを出力する

参照するプロジェクトの設定

ここではtest/tsconfig.jsonのこと。
references: []を追加する。

  {
    "compilerOptions": {
      "target": "es5",
      "module": "commonjs",
      "strict": true,
      "esModuleInterop": true
+   },
+   "references": [
+     { "path": "../src" }
+   ]
  }

この設定で、ビルド時に../srcのリビルド必要性を確認できるようになる。

tsc -b の動作確認

この状態でtestにてビルドする。

  src
├──   index.ts
├──   tsconfig.json
└──   util.ts
  test  <========= tsc -b
├──   index.ts
└──   tsconfig.json

それぞれindex.jsがないのでフルビルドされる。

$ npx tsc -b -v
[21:27:31] Projects in this build:
    * ../src/tsconfig.json
    * tsconfig.json

[21:27:31] Project '../src/tsconfig.json' is out of date because output file '../src/index.js' does not exist

[21:27:31] Building project 'C:/Users/syoum/work/sandbox/typescript/project-reference/src/tsconfig.json'...

[21:27:33] Project 'tsconfig.json' is out of date because output file 'index.js' does not exist

[21:27:33] Building project 'C:/Users/syoum/work/sandbox/typescript/project-reference/test/tsconfig.json'...

そのままもう一度ビルドするとスキップされる。これは自明だ。
jsファイルの他にd.tsファイルやtsconfig.tsbuildinfoファイルが増えている。

  src
├──   index.d.ts  <============== d.tsファイルができている
├──   index.js
├──   index.ts
├──   tsconfig.json
├──   tsconfig.tsbuildinfo  <==== ビルド情報 (差分判定に使う?)
├──   util.d.ts  <=============== d.tsファイルができている
├──   util.js
└──   util.ts
  test
├──   index.js
├──   index.ts
└──   tsconfig.json

先ほどのようにsrc/util.tsを変更してもう一度コマンドを実行してみる。

$ npx tsc -b -v
[21:33:38] Projects in this build:
    * ../src/tsconfig.json
    * tsconfig.json

[21:33:38] Project '../src/tsconfig.json' is out of date because oldest output '../src/index.js' is older than newest input '../src/util.ts'

[21:33:38] Building project 'C:/Users/syoum/work/sandbox/typescript/project-reference/src/tsconfig.json'...

[21:33:38] Updating unchanged output timestamps of project 'C:/Users/syoum/work/sandbox/typescript/project-reference/src/tsconfig.json'...

[21:33:38] Project 'tsconfig.json' is up to date with .d.ts files from its dependencies

[21:33:38] Updating output timestamps of project 'C:/Users/syoum/work/sandbox/typescript/project-reference/test/tsconfig.json'...

Project '../src/tsconfig.json' is out of date because...とあるよう..
参照先のsrcプロジェクトが変更されたことを検知しリビルドされるようになった😄

TypeScript3.0時点ではインクリメントビルドはあくまでプロジェクト単位のみ。
それでもプロダクトとテストを別プロジェクトにすれば、テストのたびにプロダクトをビルドする必要はなくなる。

また、プロダクトコードでテストモジュールを誤ってimportすることもなくなる。

Rest parameters with tuple types

HAD BETTER EASY

Rest parametersにタプルが対応した。

function foo1(...args: [number, string, boolean]) {
  console.log(args)
}

foo1(10, "ten", true)
// -> [ 10, 'ten', true ]

Spread expressions with tuple types

HAD BETTER EASY

Spread operatorのタプル版に対応した。

function foo(x: number, y: string, z: boolean) {
  console.log(x, y, z);
}

const args: [number, string, boolean] = [10, "ten", true];
foo(...args);
// -> 10 ten true

Generic rest parameters

HAD BETTER NORMAL

Rest parametersにジェネリクスを使用できるようになった。
any[]はTuple型に推論/キャプチャされる。

それを利用すると、以下のような部分適応する関数bindの型推論ができる。

declare function bind<T, U extends any[], V>(
  f: (x: T, ...args: U) => V,
  x: T
): (...args: U) => V;

実装したコード例は以下。

function bind<T, U extends any[], V>(
  f: (x: T, ...args: U) => V, // 関数fの第1引数をT、第2引数以降をU(タプル) とする
  x: T // 関数fの第2引数は 上記fの第1引数と同じ型T
): (...args: U) => V {
  // 戻り値は関数型. その引数は関数fの第2引数以降 (第1引数はbindされた)
  return (...args: U) => f(x, ...args);
}

function say(name: string, age: number, shouldAgeSecret: boolean): string {
  return `私の名前は${name}です。 歳は${shouldAgeSecret ? "秘密" : age}です。`;
}

const mimizouSay = bind(say, "みみぞう"); // 第2引数がstringと推論される
console.log(mimizouSay(333, false));
// -> 私の名前はみみぞうです。 歳は333です。

const mimizou33YearSay = bind(mimizouSay, 33); // 第2引数がnumberと推論される
console.log(mimizou33YearSay(false));
// -> 私の名前はみみぞうです。 歳は33です。

const mimizou33YearSaySecret = bind(mimizou33YearSay, true); // 第2引数がbooleanと推論される
console.log(mimizou33YearSaySecret());
// -> 私の名前はみみぞうです。 歳は秘密です。

Optional elements in tuple types

HAD BETTER EASY

タプルでも?でOptionalを表現できるようになった。

let t: [number, string?, boolean?];

t = [10];
t = [10, undefined];
t = [10, "10"];
t = [10, "10", undefined];
t = [10, "10", true];

New unknown top type

HAD BETTER EASY

安全なany型と称されるunknown型が追加された。
型が分からないものをanyと扱うのは大変危険なので、unknownを使おう。

  • 全ての型はunknown型に代入できる
  • unknown型はunknown型とany型以外には代入できない

unknown型はいかなる操作も受けつけない。(関数呼び出しやプロパティアクセスなど)

declare let strangers: unknown[];

console.log(strangers.length)
// -> unknown型のイレモノである配列は使える

console.log(strangers[0].length)
// -> unknown型のプロパティにアクセスしようとしてエラー

Narrowingで型を狭めると具体的な型として判定される。

declare let stranger: unknown;

if (typeof stranger === "string") {
    stranger.length;
    // -> strangerはstringと推論されるのでOK
}

Union TypeやIntersection Typeの挙動は以下のようになる。

  • unknown & TT
  • unknown | T (Tはany以外) は unknown
  • unknown | T (Tはany) は any

Support for defaultProps in JSX

HAD BETTER EASY

JSXでdefaultPropsに対応した。

import React from "./node_modules/@types/react";

export interface Props {
  name: string;
}

export class Greet extends React.Component<Props> {
  render() {
    const { name } = this.props;
    return <div>Hello {name.toUpperCase()}!</div>;
  }
}

// nameが指定されていないのでエラー
let el = <Greet />;
import React from "./node_modules/@types/react";

export interface Props {
  name: string;
}

export class Greet extends React.Component<Props> {
  render() {
    const { name } = this.props;
    return <div>Hello {name.toUpperCase()}!</div>;
  }
  // defaultPropsを追加できるようになった
  static defaultProps = { name: "world" };
}

// ↑でdefaultPropsを指定するとOptionalとみなしてくれる
let el = <Greet />;

/// <reference lib="..." /> reference directives

HAD BETTER EASY

<reference lib="es..." />でビルトインのlibに対して明示的に定義ファイルを指定できるようになった。

以下はtargetes5を指定したケースで、Promiseは使えない。
ところが、<reference lib="..." />でPromiseを指定すると使えるようになる。

index.ts

Promise.resolve("hoge");

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true
  }
}

index.ts

/// <reference lib="es2015.Promise" />

Promise.resolve("hoge");

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true
  }
}