TypeScript v3.8.1 RC 変更点
TypeScript 3.8 RCがアナウンスされました。
What's new in TypeScript in 3.8- Breaking Changes in 3.8
- TypeScript 3.8 Iteration Plan
- TypeScript Roadmap: July - December 2019
Roadmapはこちら。
この辺に僕が試した時のコードを投げてあります。
変更点まとめ
- 型のみのimport, exportのサポート Type-Only Imports and Exports
-
import type { foo } from "./foo";
やexport type { foo };
をサポートした - flowtypeには前からあったやつですね
-
- ECMAScript Private Fieldsのサポート ECMAScript Private Fields
-
class { #name: string; }
的なやつのサポート -
#
やだなぁ…
-
-
export * as foo ...
的な構文のサポート export * as ns Syntax-
export * as utils from "./utils";
的な構文のサポート
-
- トップレベルのawaitのサポート Top-Level await
- そのまんま
-
es2020
がtarget
とmodule
に入った Add ES2020 matchAll APIs - JSDocでの修飾子のサポート JSDoc Property Modifiers *1 *2
- JS環境下で
@public
,@private
,@protected
,@readonly
が使えるように
- JS環境下で
- より良いディレクトリ監視と
watchOptions
の追加 Better Directory Watching and watchOptions-
node_modules
がクソ重たいのでインストール直後などに過敏に反応しすぎないようにした という感じ? - tsconfig.json, jsconfig.json に
watchOptions
の項目を追加-
watchFile
,watchDirectory
,fallbackPolling
,synchronousWatchDirectory
が下にある
-
-
- ちょっと怠惰なインクリメンタルチェック “Fast and Loose” Incremental Checking
-
assumeChangesOnlyAffectDirectDependencies
の追加 - 変更されたファイルの依存ツリーをあまり厳密に追わないかわりに早くする
-
EditorとかProducitivity系の変更は新機能紹介で取り上げられなくなってきた気がしますね…。
Iteration Planからたどるとこれ以外にも結構色々改善されています。
破壊的変更!
- index signatureを含むunion型への値の代入のチェックをもうちょっと厳格にした Stricter Assignability Checks to Unions with Index Signatures
- JSDocで
object
とnoImplicitAny
が併用された時any
にならないようにしたobject
in JSDoc is No Longerany
UndernoImplicitAny
- 歴史的経緯が色々あった
-
Object
とobject
はTypeScript的には区別される - JavaScriptでは両方
any
になっていた -
object
はnoImplicitAny
と併用した場合、TypeScriptと同様の振る舞いになるようになる
- オプショナルな引数について型推論が行われない場合、
any
になるようになった (変更されたPRわからず…)- これによって
noImplicitAny
に引っかかるようになった
- これによって
型のみのimport, exportのサポート
import type { foo } from "./foo";
や export type { foo };
がサポートされました。
export interface Options {
name: string;
}
import type { Options } from "./exports";
export type { Options };
こういう感じ。
コンパイルするとimports.tsは次のようになります。
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
全部消えましたね。
基本的にTypeScriptは各ファイルの内容を把握していますし、どの値が型としてのみ使われていて、どの値が本当に値としても使われているかも判別できます。
しかし、先の例のようなimport, exportのみのような場合だと一部の例で差し障りがあるようです。
例えば Babel
経由で動かしている場合、 transpileModule
API経由で使っている場合などです。
これらのために、"importしたいのは型だけだよ"と構文上明示できる必要があります。
flowtypeには5年くらい前からあるやつです。
型だけなので当然値として使おうとすると怒られる。
import type { Foo } from "./exports";
// 型のみなので当然継承できずに怒られる
// error TS1361: 'Foo' cannot be used as a value because it was imported using 'import type'.
export class Bar extends Foo {
sayHello() {
return "Hello, bar!";
}
}
構文上の曖昧さをなくすため、現時点ではdefault importとnamed importを混在して使うことは禁止されています。
// typeのみの場合、default importとnamed importを両方同時にすることはできない
// ↓ は現時点ではエラーになる type import は A と B 両方? A のみ? パット見意味が一意にならない
// import type A, { B } from "./exports";
// 分割すればOK
import type A from "./exports";
import type { B } from "./exports";
さらに、今まで同様のimportの使い方を制御するためのオプションも追加されました。
compilerOptions
に importsNotUsedAsValues
が指定できるようになっています。
使えるのは remove
(デフォルト、今までと同じ)、 preserve
、 error
です。
// importsNotUsedAsValues が remove の場合、消える
// importsNotUsedAsValues が preserve の場合、 require("./exports"); などが残る
// importsNotUsedAsValues が error の場合、怒られる
// error TS1371: This import is never used as a value and must use 'import type' because the 'importsNotUsedAsValues' is set to 'error'.
import { A } from "./exports";
// ↓ならOK
// import type { A } from "./exports";
function foo(): typeof A {
return "a";
}
preserve
は import "./exports";
相当を書いたのと同様の結果が残ります。
importによるside-effectが欲しい場合ですね。
error
の場合、エラーになります。
強火で行きたい人はこれにしましょう。
今後、この import type
を積極的に使うべきかどうかは、プロジェクトの事情によるでしょう。
そのうちこれを検出して自動的に直してくれるlinterなりが出る気はするので、困ったらしっかり使う、くらいの感じでよいのではないでしょうか。
今のところimportの自動補完でも type
無しで行われるっぽいので手で直すのめんどくさいし…。
ECMAScript Private Fieldsのサポート
出ましたね #foo
とかいうキモいsyntaxのあいつです…。
利用する時は WeakMap
をpolyfillとして使うので target
が es2015
以上が必要です。
// error TS18028: Private identifiers are only available when targeting ECMAScript 2015 and higher.
class Person {
#name: string
private fav?: string;
// 名前空間が分かれてるので同名のプロパティが存在してもよい
name: string;
// 他の可視性制御の修飾子と併用したらダメ
// error TS18010: An accessibility modifier cannot be used with a private identifier.
// public #other1?: string;
// private #other2?: string;
// protected #other3?: string;
// これもダメ abstract class で定義しても次のエラーが出る
// error TS18019: 'abstract' modifier cannot be used with a private identifier
// abstract #other4?: string;
// これはOKらしい
readonly #other5?: string;
constructor(name: string, fav?: string) {
this.#name = name;
this.name = name;
this.fav = fav;
}
greet() {
console.log(`Hello, my name is ${this.#name}!${this.fav ? ` I like ${this.fav}.` : ""}`);
}
}
class Person2 extends Person {
// 継承しても親から引き継がない 独立している
#name: string;
constructor(name: string) {
super(name + "👍");
this.#name = name;
}
greet2() {
console.log(`Hi, I'm ${this.#name}.`);
}
}
let papyrus = new Person("Papyrus", "🐾");
// Hello, my name is Jeremy Bearimy! I like 🐾. と表示される
papyrus.greet();
let sans = new Person2("Sans");
// Hello, my name is Sans👍! と表示される
sans.greet();
// Hi, I'm Sans. と表示される
sans.greet2();
// クラスの外側からはアクセスできない
// error TS18013: Property '#name' is not accessible outside class 'Person' because it has a private identifier.
// papyrus.#name
// 型上にはちゃんと存在しているので、該当のprivate fieldがないとエラーになる
// 実質、Person型かそれを継承したクラスのインスタンス縛りになる
// error TS2741: Property '#name' is missing in type '{ fav: string; name: string; greet(): void; }' but required in type 'Person'.
// let mimic1: Person = {
// fav: "",
// name: "",
// greet() { },
// };
// これはOK
let mimic2: Person = sans;
// esnextで .js 出せばわかるけど #name 的なのは宣言必須
private fieldは完全に他の名前空間から独立し、そのクラス自身にのみ紐付きます。
クラス外部からアクセスすることはできません。
これには子クラスも含まれます!
なので、private fieldは完全に隠蔽され追加・変更・削除を自由にやっても他を壊す/壊されることがないので安心です。
transpileされたコードを見るとどうなっているかわかりやすいので、コンパイル後のJSコードを抜粋します。
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to set private field on non-instance");
}
privateMap.set(receiver, value);
return value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to get private field on non-instance");
}
return privateMap.get(receiver);
};
var _name;
class Person {
constructor(name) {
_name.set(this, void 0);
__classPrivateFieldSet(this, _name, name);
}
greet() {
console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}!`);
}
}
_name = new WeakMap();
クラスの種類毎にプロパティ保持用のWeakMapが作成されています。
このWeakMapは他のモジュールや子クラスなどの外部に公開されないことがわかります。
export * as foo ...
的な構文のサポート
できるようになったらしいです。
export function hello() {
console.log("Hello!");
}
export function bye() {
console.log("Bye...");
}
// 今までのやり方
import * as a from "./libs";
export { a };
// まぁ普通にアクセスできる
// a.hello();
// 上記のimport, exportを一度にやる!
export * as b from "./libs";
// b はこのモジュール中には公開されないっぽい
// b.hello();
import { a, b } from "./";
a.hello();
a.bye();
b.hello();
b.bye();
この構文でexportした識別子は同モジュール中で参照できないようです。
トランスパイルされたJSを見るとprivateっぽい振る舞いになっていることが汲み取れます。
個人的には export * from "./foo";
すら稀に使うなーくらいだったので使う機会あまりなさそう。
トップレベルのawaitのサポート
割と嬉しいやつ〜〜〜
target
が es2017
以降、 module
が esnext
か system
じゃないとダメだそうです。
// target が es2017 以降で module が esnext か system じゃないとダメ
// es2017 以降だと await がキーワードになっているので
function timeout(milliseconds: number) {
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
})
}
console.log("Just 1 sec");
await timeout(1000);
console.log("dit it!");
// export とか import とかないとモジュールだと思ってもらえないやつ
// --isolatedModules でもいいらしい(試してない)
// error TS1375: 'await' expressions are only allowed at the top level of a file when that file is a module, but this file has no imports or exports. Consider adding an empty 'export {}' to make this file a module.
export { }
bundlerがいい感じにしてくれないと生変換でそのまま使うのは時代を先取りすぎそう…。
es2020
が target
と module
に入った
らしいです。
optional chaining, nullish coalescing, export * as ns
, dynamic import,
bigint literalあたりが含まれているそうな。
JSDocでの修飾子のサポート
らしいです。
前からなかったっけこれ…?
JavaScriptとか全然やらないのでわからん。
// @ts-check
class Foo {
constructor() {
/** @private */
this.stuff = 100;
}
printStuff() {
console.log(this.stuff);
}
}
// error TS2341: Property 'stuff' is private and only accessible within class 'Foo'.
new Foo().stuff;
@public
, @private
, @protected
, @readonly
が使えるようになったらしいですね。
より良いディレクトリ監視と watchOptions
の追加
tsconfig.json(とjsconfig.json)に compilerOptions
と同じレベルに watchOptions
が追加されました。
なんか今までも TSC_WATCHFILE
, TSC_WATCHDIRECTORY
というのがあったらしいですね…。
まったく知らんかった…。v2.8くらいからあったらしいです。
node_modules
の更新時とかに激烈にCPU使ってしまったりするのを制御する目的っぽい。
{
// 既存の設定色々…
"watchOptions": {
// fixedPollingInterval, priorityPollingInterval, dynamicPriorityPolling, useFsEvents, useFsEventsOnParentDirectory
// useFsEvents がデフォルトで今まで通り
"watchFile": "useFsEvents",
// fixedPollingInterval, dynamicPriorityPolling, useFsEvents
// useFsEvents がデフォルトで今まで通り
"watchDirectory": "useFsEvents",
// useFsEvents を選択してた時に利用できなかったらどの戦略で行くか かな
// fixedPollingInterval, priorityPollingInterval, dynamicPriorityPolling
"fallbackPolling": "dynamicPriority",
"synchronousWatchDirectory": false
}
}
こんな感じ。
設定項目は4つある。
英語が正しく読めてるかちょっと自信ない…!
-
watchFile
単体のファイルをどう監視するかの設定-
fixedPollingInterval
一定間隔で毎秒何回かチェック -
priorityPollingInterval
一定間隔でチェックするがあんま重要そうじゃなさそうなやつはちょっと頻度落とす -
dynamicPriorityPolling
動的なキューに入れてチェック 微妙そうなヤツは微妙なりの頻度 -
useFsEvents
デフォルト。OSネイティブのファイルシステムからのイベントで駆動 -
useFsEventsOnParentDirectory
対象ファイルを含むディレクトリの変更を利用する。負荷は下がるが精度も下がるかも
-
-
watchDirectory
再帰的なディレクトリ監視ができるシステムでも振る舞いの設定-
fixedPollingInterval
一定間隔で毎秒何回かチェック -
dynamicPriorityPolling
動的なキューに入れてチェック 微妙そうなヤツは微妙なりの頻度 -
useFsEvents
デフォルト。OSネイティブのファイルシステムからのイベントで駆動
-
-
fallbackPolling
ファイルシステムからのイベントで動かす設定かつ、それがサポートされない環境だった時の設定-
fixedPollingInterval
前に同じ -
priorityPollingInterval
前に同じ -
dynamicPriorityPolling
前に同じ
-
-
synchronousWatchDirectory
ディレクトリの遅延監視を無効にする。ごく少数のセットアップでは役に立つかも。
って感じらしいです。
現状特に困ってないなら使わなくてよいんじゃないでしょうか…。
ちょっと怠惰なインクリメンタルチェック
assumeChangesOnlyAffectDirectDependencies
というオプションが導入されたそうです。
--watch
モードに影響があるそうな…。
ファイルA.ts←ファイルB.ts←ファイルC.ts←....
というファイル間の依存関係があった場合、今まではファイルAを変更するとファイルB, Cという具合に参照先すべてをチェックしなおしていました。
assumeChangesOnlyAffectDirectDependencies
を true
に設定すると、ファイルAを変更した後、1つ先のファイルBまでしかチェックされないようになるらしいです。
多くの場合ではそれで十分な挙動だと思います…!
普段 --watch
相当の機能や --incremental
とか全然使ってないのでわからん…!
もしかしたら陰ながら恩恵の預かれたりするのかな…。
index signatureを含むunion型への値の代入のチェックをもうちょっと厳格にした
説明が難しいのでコード見て感じてください。
let obj1: { [x: string]: number } | { a: number };
// 余計かつ型的にマッチしないものがあると怒られるようになった
// error TS2322: Type 'string' is not assignable to type 'number'.
obj1 = { a: 5, c: 'abc' };
// もちろんこれはOK
obj1 = { a: 5, c: 3 };
// object literalの直接代入じゃなければ許されるのは相変わらず
let obj1a = { a: 5, c: 'abc' };
obj1 = obj1a;
let obj2: { a: number };
// 余計なものがあると怒られるヤツとはエラーメッセージが異なる…! (昔からエラー)
// error TS2322: Type '{ a: number; c: string; }' is not assignable to type '{ a: number; }'.
// Object literal may only specify known properties, and 'c' does not exist in type '{ a: number; }'.
obj2 = { a: 5, c: 'abc' };
let obj3: { [x: string]: number } | { [x: number]: number };
// これも怒られる。今までなんでOKだったんや…
// error TS2322: Type 'string' is not assignable to type 'number'.
obj3 = { a: 'abc' };
export { }
なんかこれは今までもエラーだったのでは…?という気がするけどそうでもないっぽい。
$ npx -p typescript@v3.7 tsc ./src/indexSignatureAndUnion/basic.ts
$ npx -p typescript@rc tsc ./src/indexSignatureAndUnion/basic.ts
v3.7.5 と v3.8.1 を比べるとたしかに前は素通しになっているのであった。
JSDocで object
と noImplicitAny
が併用された時 any
にならないようにした
らしいです。
歴史的経緯とか文化より、JSDocでは Object
は any
相当に評価されていた。
これはまぁなんでもいいよの意図と捉えるのが自然なので…。
で、 object
(先頭小文字) も any
としてたんだけど、v3.8からTypeScriptの型の評価にあわせる感じにした、という経緯のようです。
オプショナルな引数について型推論が行われない場合、 any
になるようになった
らしいです。
function foo(f: () => void) {
}
// error TS7006: Parameter 'param' implicitly has an 'any' type.
foo((param?) => {
});
型アノテーションなしで param?
だけ書くという発想がなかった…。
なるほどね?