[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
これを解決するには必ず以下の順で処理をしなければいけない。
src
配下でtsc -b
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 & T
はT
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に対して明示的に定義ファイルを指定できるようになった。
以下はtarget
にes5
を指定したケースで、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
}
}