TypeScript 4.4 変更点 まとめ
TypeScript 4.4
TypeScript 4.4.0 (Beta) が 2021/06/29 に公開された.4.4.1 (RC) は 08/06,4.4.2 (Release) は 08/24 に公開される予定.4.4.1 (RC) は08/13に公開された.
-
Iteration Plan
- 4.4のリリースを扱うIssue
-
Announcing TypeScript 4.4 Beta
- Betaの新機能紹介
- Announcing TypeScript 4.4 RC
- PlaygroundにBetaが入ってきた
私の個人的なまとめにとどまらないほうが嬉しいので,コメントは歓迎しています.
なぜスクラップにしたのか?
4.3のときは普通の記事として投稿したが,言ってしまえばリリースノートを読んでコピペしてるだけの記事なので,「記事の質が低くて英語ドキュメント読んだほうが早い」という意見の攻撃範囲に入っており,勝手にダメージを受けてしまう.これを避けるために,スクラップならただの翻訳記事でもいいでしょ (むしろそのための機能じゃないか) という言い訳で武装してスクラップとして投稿することにした.
TS 4.3の記事はBetaで書いたのをRelease用に更新するか,それともRelease用の記事を新しく公開するかで悩んだ挙句何もしておらず,Betaの古い情報をぶん投げたままみたいな状態になってしまっている.コメントの集合であるスクラップは,新バージョンについてBetaからReleaseに向かって追い続けるという趣旨にちょうどマッチしているようにも思っている.
Control Flow Analysis of Aliased Conditions
TypeScriptには control flow analysis もしくは type guard という機能があり,if
などの制御構文で型チェックを書くと自動で型が絞り込まれる.
function foo(v: unknown) {
const isString = typeof v === "string";
if (isString) {
// ここではvはstring
console.log(v.toUpperCase());
}
}
TS 4.4 以前では,型チェックを行っている箇所を変数に代入すると絞り込みが働かなくなっていたが,4.4ではちゃんと絞り込まれるようになる.チェックの結果を複数の箇所で使いまわすのがJavaScriptでの自然な形で書ける.
function foo(v: unknown) {
const isString = typeof v === "string";
if (isString) {
// 4.3: vはunknown
// 4.4: vはstring
console.log(v.toUpperCase()); // 4.3 だとここでエラー
}
}
ただし,代入先は const
な変数でなければならない.
function foo(v: unknown) {
// let はだめ
let isString = typeof v === "string";
if (isString) {
// 4.3, 4.4: vはunknown
console.log(v.toUpperCase()); // 型エラー
}
}
readonly
なプロパティでもよいようにも読めるのだが働かない.
function foo3(v: unknown) {
const obj = {
isString: typeof v === "string",
} as const;
if (obj.isString) {
// vはunknown
console.log(v.toUpperCase());
}
}
typeof
演算子以外に,&&
などでのチェックにも適用される.
function doSomeChecks(
inputA: string | undefined,
inputB: string | undefined,
shouldDoExtraWork: boolean
) {
// 記事ではletになってるがこれだと動かない
const mustDoWork = inputA && inputB && shouldDoExtraWork;
if (mustDoWork) {
// Can access 'string' properties on both 'inputA' and 'inputB'!
const upperA = inputA.toUpperCase();
const upperB = inputB.toUpperCase();
// ...
}
}
いったん代入した条件をさらに論理演算で組み合わせて代入してもかまわない (一定以上の深さになると推論できないとされている).
function f2(x: "foo" | "bar" | "baz") {
const isBar = x === "bar";
const isBaz = x === "baz";
const isBarOrBaz = isBar || isBaz;
if (isBarOrBaz) {
x; // "bar" | "baz"
} else {
x; // "foo"
}
}
もちろん,いわゆるtagged unionパターンでも動く.
function map<T, R>(
f: (t: T) => R,
o: { type: "Some"; value: T } | { type: "None" }
) {
const isSome = o.type === "Some";
if (isSome) {
return f(o.value);
} else {
return o;
}
}
tagged unionのタグ部分を分割代入してもきちんと推論される.
function map<T, R>(
f: (t: T) => R,
o: { type: "Some"; value: T } | { type: "None" }
) {
const { type } = o;
if (type === "Some") {
return f(o.value);
} else {
return o;
}
}
Symbol and Template String Pattern Index Signatures
最初のpull request,リテラル型なども許容
実際にマージされたpull request,リテラル型などを不許可Generalized Index Signatures という名前で TS 4.2 とかのIteration Planにも載ってたやつ.
4.3まで,インデックスシグネチャのキーの型には string
か number
か string | number
しか置くことができなかったが,これを緩和し,symbol
やテンプレート文字列型を許容するようになる.
interface Data {
[opt: `data-${string}`]: unknown;
}
const data: Data = {
// 「data-」から始まるプロパティのみを受け付ける
"data-foo": 3,
"data-bar": 42,
};
const invalidData: Data = {
// 「data-」で始まっていないのでエラー
foo: 3,
bar: 42,
};
interface SymbolDict {
[sym: symbol]: unknown;
}
const alpha = Symbol();
const bravo = Symbol();
const sdict: SymbolDict = { [alpha]: 41, [bravo]: 42 };
なお,Generalized Index Signaturesと呼ばれていたものと異なり,リテラル型や型引数は依然として置くことができない.リテラル型はわざわざインデックスシグネチャを使わずとも外延的に書くことができるからだろう.
interface Literal {
// An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead. ts(1337)
[opt: "foo" | "bar"]: number;
}
// Quick Fix でMapped typesを使った形に直される.
type Literal = {
[opt in "foo" | "bar"]: number;
};
// Mapped typesはこのような型になる
type NervousLiteral ={
foo: number;
bar: number;
};
unknown
type in Catch Variables
Defaulting to the
コンパイルオプション --useUnknownInCatchVariables
を追加する.このオプションを有効化すると,try-catch文の catch (e)
で導入した変数 e
がデフォルトで unknown
型になる.
// throw しそうな関数
declare function doSomethingDangerous(): void;
try {
doSomethingDangerous();
} catch (error) {
// デフォルト: errorはany型
// --useUnknownInCatchVariables: errorはunknown型
console.error(error.message); // オプションがtrueのときエラー
}
TS 4.0 で catch(error: unknown)
という明示的な指定が可能になったのに続き,これをデフォルトの挙動にするオプションになっている.--useUnknownInCatchVariables
下では,逆に catch(error: any)
とすることで明示的に any
を使うことができる.
このオプションは strict
に含まれるため,strict:true
なコードベースでは catch (error)
して得たerrorをanyとして扱っているコードが動かなくなる.
厳密なオプショナルプロパティ
JavaScriptでは存在しないプロパティにアクセスするとundefinedが返る.一方で存在するプロパティにundefinedが入っている場合もありうる.
const x = {a: 3};
const y = {a: 3, b: undefined};
x.b; // xにプロパティbは存在しないのでundefined
y.b; // yのプロパティbの値はundefined
多くのコードでこれらのパターンは同一視されてきたので,TypeScriptはオプショナルなプロパティはundefinedとのユニオン型になるようにしてきた.
interface Person {
name: string;
age?: number; // age?: number | undefined になる
}
// Personとまったく同じ型
interface SameAsPerson {
name: string;
age?: number | undefined;
}
const p: Person = {
name: "Makima",
age: undefined, // undefinedを代入できる
};
TS 4.4 で導入される新しいコンパイルオプション --exactOptionalPropertyTypes
を有効化すると,オプショナルなプロパティに勝手に | undefined
が付かないようになる.
interface Person {
name: string;
age?: number; // age?: number のまま
}
const p: Person = {
name: "Makima",
// Type 'undefined' is not assignable to type 'number'. ts(2322)
age: undefined, // undefinedを代入できない
};
p.age; // プロパティアクセスは今まで通り number | undefined になる
これは「undefinedの代入可否」と「プロパティの省略可否」が直交するようになったとも表現できる.
|undefinedを| required | optional |
|---|---|---|---|
|代入できない|prop: T
| prop?: T
|
|代入できる| prop: T | undefined
| prop?: T | undefined
|
存在しないプロパティと存在するプロパティの区別がつくようになると,in
などの動きを正確に表せるようになってうれしい.
'age' in p
はプロパティ 'age'
が p
に存在するときに true
を返す.今までは「p.ageは存在するが値がundefined」という可能性があったのでオプショナルなプロパティの絞り込みに使えなかったが,これで使えるようになった.p.age !== undefined
でもあまり困らないが.
const p: Person = {
name: "Makima",
};
if ("age" in p) {
// デフォルトではnumber | undefined
// --exactOptionalPropertyTypes ではnumber
p.age;
}
オブジェクトのスプレッド構文も存在しないプロパティとundefinedが入ったプロパティを区別する.
const x = {b: 23} // aが存在しない
const y = {a: undefined, b: 23}; // aが存在して値がundefined
const xx = {a:42, ...b};
const yy = {a:42, ...c};
console.log(xx.a); // 42
console.log(yy.a); // undefined
そのほか Object.assign
,Object.keys
,for-inループなどでプロパティの存在有無が厳密に区別される.
このオプションは --strict
には含まれず,別途有効化する必要がある.また当然ながら --strictNullChecks
が無効だとはたらかない.
より実践的な立場に立った解説:
tsc --help
色付きでいい感じに整形されて表示されるようになった.コマンドやオプションの説明文も変わってる.
Performance Impovements
速くなったらしい.
Spelling Suggestions for JavaScript
.tsファイルで存在しない変数名を書いていて,それが既存の変数名と似ている場合,「もしかして」的にサジェストを出してくれる機能があるが,.jsファイル内でもスペルミスを検出してくれるようになった.
const message = "momi momi...";
// Could not find name 'massage'. Did you mean 'message'? ts(2570)
console.log(massage);
configファイルなどJSを回避できない場面はいまだ多いのでありがたい.
Abstract Properties Do Not Allow Initializers
破壊的変更.抽象クラスの abstract
プロパティを初期化するとコンパイルエラーになる.そもそも以前からこの初期化はトランスパイル時に消えていたようだ.
abstract class K {
// Property 'v' cannot have an initializer because it is marked abstract. ts(1267)
abstract v = 3;
}
Inlay Hints
VS CodeでRustを書いたときに出るような薄い文字での型ヒントを追加しようとしているみたい.型パズルと格闘するときなど,型の内容をチェックするのにカーソルをホバーしなくて済むのがうれしそう.
https://devblogs.microsoft.com/typescript/announcing-typescript-4-4-beta/#inlay-hints より引用
VS Code側での対応は完了 (https://github.com/microsoft/vscode/pull/113412) しており,Insidersビルドで利用可能.デフォルトでは表示されず,設定からそれぞれの表示を有効化する必要がある.とりあえず全部有効化したい場合はsettings.jsonを以下のようにする.
{
"typescript.inlayHints.parameterNames.enabled": "all",
"typescript.inlayHints.enumMemberValues.enabled": true,
"typescript.inlayHints.functionLikeReturnTypes.enabled": true,
"typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": true,
"typescript.inlayHints.parameterTypes.enabled": true,
"typescript.inlayHints.propertyDeclarationTypes.enabled": true,
"typescript.inlayHints.variableTypes.enabled": true,
}
master
→ main
4.4とは直接関係ないが,デフォルトブランチがmasterからmainに改名された.
Class Static Blocks
今回のTC39 Proposal枠.
なぜかBetaのアナウンスに含まれていなかったようで,
を読んで初めて気が付いた.クラス内に static
から始まるブロックを書くことができる.このブロックに書かれたコードはクラスの定義・評価時に実行される.
class C {
static {
console.log('hello!');
}
}
const c = class {
static {
console.log('hello!');
}
};
クラス定義のそばに即時関数式が並んでると考えるとよい.実際そのようにトランスパイルされる.
class CC {
}
(() => {
console.log('hello!');
})();
// ↓クラス式が評価されるタイミングで実行されることがわかる
const c = (_a = class {
},
(() => {
console.log('hello!');
})(),
_a);
class
の中なのでプライベート (#
) なプロパティ/メソッドにもアクセスできる.static
なプロパティはもちろん,インスタンスのプロパティでもかまわない.
let getFooOf: (k: K) => number;
let setFooOf: (k: K, value: number) => void;
class K {
#foo = 42;
static {
getFooOf = k => k.#foo;
setFooOf = (k, value) => k.#foo = value;
// この中でthisはK
console.log(new this().#foo);
}
}
const k = new K();
console.log(getFooOf!(k));
// let変数への代入は必ず行われるのでここで未初期化のコンパイルエラーが出るのは正しくない気がする.ワークアラウンドとして!をつけている
setFooOf!(k, 3234);
console.log(getFooOf!(k));
Betaの不具合として,クラス式のStatic Blockからクラス自身を参照できない.
(class X {
static msg = "wryyy";
static {
// ここでX is not definedになる
console.log(X.msg);
}
})
また,static {}
内のコードが制御フロー解析の対象にならないというバグを見つけたので報告しておいた.