Microsoftは、多数の新機能を提供するオープンソースプログラミング言語の最新版「TypeScript 3.7」を公開した。オプショナルチェイニングの実装やnullish coalescing演算子の導入といった改善を加えた。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
Microsoftは2019年11月5日(米国時間)、オープンソースのプログラミング言語の最新版「TypeScript 3.7」を公開した。
TypeScriptは、静的型付けができる言語で、JavaScriptのスーパーセット。ECMA規格に従った最新のJavaScriptの機能を、古いWebブラウザやランタイムが扱えるようにコンパイルすることもできる。
TypeScript 3.7は、NuGetを使うか、次のコマンドラインのように、npmを使ってインストールできる。
npm install typescript
TypeScript 3.7は「Visual Studio 2019」「Visual Studio 2017」の他、「Visual Studio Code」と「Sublime Text」でも利用できる。TypeScript 3.7の主な特徴は次の通り。
TypeScriptのビルトインフォーマッターにも改良を加えた。JavaScriptの自動セミコロン挿入(ASI)ルールによって、行末のセミコロンがオプションとなっている場所で、セミコロンの挿入と削除を自動化できるようになった。
この設定は「Visual Studio Code Insider」で利用でき、「Visual Studio 16.4 Preview 2」でも、「ツールオプション」メニューで利用できる。
「insert」(挿入)または「remove」(削除)の値を選ぶと、TypeScriptサービスで提供される自動インポートや抽出した型、他の生成コードのフォーマットにも影響する。設定をデフォルト値「ignore」(無視)のままにすると、現在のファイルで検出されたセミコロンの傾向に合わせて生成コードを整形する。
TypeScriptのWebサイト最上部のタブからアクセスできる公式の「Playground」(試用サイト)では、迅速なエラー修正やダーク/ハイコントラストモード、他のパッケージをインポートする際の自動的な型取得といった新機能が利用できるようになった。
さらに、「What's new」メニューから、インタラクティブなコードスニペットを使ったTypeScript 3.7の新機能の解説を閲覧できる。
加えて、TypeScriptのWebサイトの右上隅に検索フィールドを用意し、ハンドブックやリリースノートなどを対象にキーワード検索ができるようになった。この検索機能は、検索APIサービス「Algolia」を使って提供されている。
TypeScript 3.7では、ECMAScriptで最も要望の高い機能の一つであるオプショナルチェイニングを実装した。
オプショナルチェイニングの基本的な働きは、実行時にnullやundefinedが発生した場合、式の実行を直ちに停止できるようなコードを作成可能にすることだ。このためにオプションプロパティアクセスに向けて新しい「?.」演算子を追加した。
let x = foo?.bar.baz();
例えば、このコードスニペットは、次のような記述と同じことになる。
let x = (foo === null || foo === undefined) ?
undefined :
foo.bar.baz();
nullish coalescing演算子は、オプショナルチェイニングと密接に関係するECMAScriptの機能だ。演算子として「??」を用いる。
この機能は、nullやundefinedを処理する際に、デフォルト値に“フォールバックする”手法の一つだ。
let x = foo ?? bar();
例えば、このコードスニペットと同じ処理を実現するために、これまでは次のように記述していた。
let x = (foo !== null && foo !== undefined) ?
foo :
bar();
予期せぬ状態が発生すると、エラーをスロー(throw)する関数がある。こうした関数は“アサーション”関数と呼ばれる。
TypeScript 3.7では、アサーション関数をモデル化する“アサーションシグネチャ”という新しい考え方を導入した。
アサーションシグネチャの導入作業の一環として、どこでどの関数が呼び出されるかをより詳細にエンコードする必要が生じた。このことが、neverを返す関数のサポートを拡大するきっかけになったという。
関数がneverを返すのは、例外をスローしたか、停止エラー条件が発生したか、プログラムが終了したかのいずれかに該当する。
TypeScriptではこれまで、関数がundefinedを返した可能性がないこと、あるいは全てのコードパスから効果的に戻らなかったことを確認するには、次のように、関数の末尾にシンタックス上のシグナル(returnまたはthrow)が必要だった。このようにしてユーザーは、失敗した関数をreturnするようにしていた。
function dispatch(x: string | number): SomeType {
if (typeof x === "string") {
return doThingWithString(x);
}
else if (typeof x === "number") {
return doThingWithNumber(x);
}
return process.exit(1);
}
TypeScript 3.7では、こうしたneverを返す関数が呼び出されると、それらの関数が制御フローグラフに影響することを認識し、対処するようになった。
function dispatch(x: string | number): SomeType {
if (typeof x === "string") {
return doThingWithString(x);
}
else if (typeof x === "number") {
return doThingWithNumber(x);
}
process.exit(1);
}
TypeScriptでは、「--declaration」フラグを使って、「.ts」や「.tsx」ファイルのようなソースTypeScriptファイルから、「.d.ts」ファイル(宣言ファイル)を生成できる。.d.tsファイルを使用すると、オリジナルのソースコードを再チェック、ビルドすることなく、他のプロジェクトに対して型チェックを行うことができる。
残念ながら、--declarationには、これまで制限があった。TypeScriptとJavaScriptの入力ファイルを混合できる「--allowJs」のような設定とともに使えなかったのだ。
TypeScript 3.7では、この2つの機能を組み合わせて利用できるようになった。--allowJsを使う場合、TypeScriptはベストエフォートでJavaScriptソースコードを理解し、同等の表現で.d.tsファイルに保存する。例えば、次のようなコードを扱ったとしよう。
/**
* @callback Job
* @returns {void}
*/
/** Queues work */
export class Worker {
constructor(maxDepth = 10) {
this.started = false;
this.depthLimit = maxDepth;
/**
* NOTE: queued jobs may add more items to queue
* @type {Job[]}
*/
this.queue = [];
}
/**
* Adds a work item to the queue
* @param {Job} work
*/
push(work) {
if (this.queue.length + 1 > this.depthLimit) throw new Error("Queue full!");
this.queue.push(work);
}
/**
* Starts the queue if it has not yet started
*/
start() {
if (this.started) return false;
this.started = true;
while (this.queue.length) {
/** @type {Job} */(this.queue.shift())();
}
return true;
}
}
このコードを次のような.d.tsファイルに変換する。
/**
* @callback Job
* @returns {void}
*/
/** Queues work */
export class Worker {
constructor(maxDepth?: number);
started: boolean;
depthLimit: number;
/**
* NOTE: queued jobs may add more items to queue
* @type {Job[]}
*/
queue: Job[];
/**
* Adds a work item to the queue
* @param {Job} work
*/
push(work: Job): void;
/**
* Starts the queue if it has not yet started
*/
start(): boolean;
}
export type Job = () => void;
型エイリアスには常に、再帰的な参照を使った場合に制限があった。型エイリアスは、どのように使用する場合でも、自身が表す型を必ず代替できなければならないからだ。だが場合によっては実現不可能である。そのためコンパイラは、次のような特定の再帰的なエイリアスを排除していた。
type Foo = Foo;
TypeScript 3.7では、型エイリアスの“トップレベル”で型引数の解決が先送りされた。例えば、JSONを表現する次のようなコードを例に挙げよう。
type Json =
| string
| number
| boolean
| null
| JsonObject
| JsonArray;
interface JsonObject {
[property: string]: Json;
}
interface JsonArray extends Array<Json> {}
TypeScript 3.7ではヘルパーインタフェースを使わずに、次のように書き換えられるようになった。
type Json =
| string
| number
| boolean
| null
| { [property: string]: Json }
| Json[];
また、今回の制限緩和により、タプルで型エイリアスを再帰的に参照することも可能になった。これまではエラーだった次のコードが、有効なTypeScriptコードになる。
type VirtualNode =
| string
| [string, { [key: string]: any }, ...VirtualNode[]];
const myNode: VirtualNode =
["div", { id: "parent" },
["div", { id: "first-child" }, "I'm the first child"],
["div", { id: "second-child" }, "I'm the second child"]
];
Microsoftは、パブリッククラスフィールドの標準化が当初の想定とは異なる方向に進みそうなことに対応し、TypeScriptへ徐々に変更を加えている。その一環として、新しい「useDefineForClassFields」フラグを提供した。このフラグによる変化のうち、最も大きなものは次の2つだ。
これにより、継承を使用する既存コードにさまざまな影響が及ぶ。第1に、ベースクラスからの「set」アクセサがトリガーの対象とならず、完全に上書きされる。
第2に、クラスフィールドを使って、ベースクラスからのプロパティを特別に処理することができない。
このことから、プロパティとアクセサを組み合わせると、問題が起こることが予想できる。さらに、イニシャライザーがないプロパティの再宣言も、問題につながる。
TypeScript 3.7は、アクセサに関する問題を検出するため、「.d.ts」ファイルの「get/set」アクセサをエミットにする。これによってTypeScriptが、上書きされたアクセサをチェックできる。
クラスフィールドの変更で影響を受けるコードは、フィールドイニシャライザーをコンストラクタ本体の割り当てに変換することで、問題を回避できる。
class Base {
set data(value: string) {
console.log("data changed to " + value);
}
}
class Derived extends Base {
constructor() {
this.data = 10;
}
}
先ほど紹介した第2の問題を軽減するため、明示的なイニシャライザーを追加したり、プロパティにはemitがないことを示すdeclare修飾子を追加したりすることもできる。
interface Animal { animalStuff: any }
interface Dog extends Animal { dogStuff: any }
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
declare resident: Dog;
// ^^^^^^^
// 'resident' now has a 'declare' modifier,
// and won't produce any output code.
constructor(dog: Dog) {
super(dog);
}
}
Microsoftによると、現在、ECMASscript 5(ES5)以降をターゲットとする場合は、useDefineForClassFieldsのみが利用できる。ES3にはObject.definePropertyが存在しないからだ。
TypeScriptのプロジェクト参照を使うと、コードベースを分割してコンパイルを容易に高速化できる。ただし依存関係を設定していなかったり、コードが古かったりすると動作しない。
TypeScript 3.7では、依存関係のあるプロジェクトを開く場合、処理系が自動的にソースの.ts/.tsxファイルを使用する。これにより、セマンティック操作が最新になっていて、きちんと機能していれば、プロジェクト参照を使用するプロジェクトであっても、編集エクスペリエンスが向上する。
このような挙動が必要ない場合は、コンパイラオプション「disableSourceOfProjectReferenceRedirect」で無効にできる。
関数の呼び出しを忘れるのは、プログラマーにとって珍しいことではないが、危険だ。関数に引数がなかったり、プロパティと思われるような名前が付いていたりする場合に忘れやすい。
例えば、次のコードは、「isAdministrator」を呼び出すのを忘れており、管理者以外のユーザーであっても構成を編集できてしまう。
interface User {
isAdministrator(): boolean;
notify(): void;
doNotDisturb?(): boolean;
}
// later...
// Broken code, do not use!
function doAdminThing(user: User) {
// oops!
if (user.isAdministrator) {
sudo();
editTheConfiguration();
}
else {
throw new AccessDeniedError("User is not an admin");
}
}
TypeScript 3.7では、このような部分を特定して、次のように、問題のある箇所を伝える。
function doAdminThing(user: User) {
if (user.isAdministrator) {
// ~~~~~~~~~~~~~~~~~~~~
// error! This condition will always return true since the function is always defined.
// Did you mean to call it instead?t
今回のチェック機能はこれまでのTypeScriptの挙動とかなり異なる。そのため、非常に「保守的」な動作をするようにした。エラーを判定するのはif内だけだ。つまり、オプションプロパティやstrictNullChecksがオフの場合や、後ほどifの本体内で関数が呼び出されている場合には、次の例のようにエラーにはならない。
interface User {
isAdministrator(): boolean;
notify(): void;
doNotDisturb?(): boolean;
}
function issueNotification(user: User) {
if (user.doNotDisturb) {
// OK, property is optional
}
if (user.notify) {
// OK, called the function
user.notify();
}
}
TypeScriptでは、ごく単純なコードが長いエラーメッセージを表示させてしまう場合がある。例えば、このようなコードを記述したとしよう。
type SomeVeryBigType = { a: { b: { c: { d: { e: { f(): string } } } } } }
type AnotherVeryBigType = { a: { b: { c: { d: { e: { f(): number } } } } } }
declare let x: SomeVeryBigType;
declare let y: AnotherVeryBigType;
y = x;
TypeScriptの従来バージョンでは、次のようなエラーメッセージを表示していた。
Type 'SomeVeryBigType' is not assignable to type 'AnotherVeryBigType'.
Types of property 'a' are incompatible.
Type '{ b: { c: { d: { e: { f(): string; }; }; }; }; }' is not assignable to type '{ b: { c: { d: { e: { f(): number; }; }; }; }; }'.
Types of property 'b' are incompatible.
Type '{ c: { d: { e: { f(): string; }; }; }; }' is not assignable to type '{ c: { d: { e: { f(): number; }; }; }; }'.
Types of property 'c' are incompatible.
Type '{ d: { e: { f(): string; }; }; }' is not assignable to type '{ d: { e: { f(): number; }; }; }'.
Types of property 'd' are incompatible.
Type '{ e: { f(): string; }; }' is not assignable to type '{ e: { f(): number; }; }'.
Types of property 'e' are incompatible.
Type '{ f(): string; }' is not assignable to type '{ f(): number; }'.
Types of property 'f' are incompatible.
Type '() => string' is not assignable to type '() => number'.
Type 'string' is not assignable to type 'number'.
このエラーメッセージは正しいが、似たようなテキストがずらずらと並び、ユーザーにとっては煩わしく、重要な情報を見つけにくい。
そこでTypeScript 3.7では、次のように簡潔にエラーメッセージを表示するようにした。
Type 'SomeVeryBigType' is not assignable to type 'AnotherVeryBigType'.
The types returned by 'a.b.c.d.e.f()' are incompatible between these types.
Type 'string' is not assignable to type 'number'.
TypeScript 3.7では、TypeScriptファイルの先頭に「// @ts-nocheck」コメントを追加することで、セマンティックチェックを無効にできるようになった。
このコメントはこれまで、checkJsを使う場合にJavaScriptソースファイルでのみ用いられていたが、全てのユーザーが簡単に移行できるように、TypeScriptファイルでも新たにサポートした。
Microsoft、プログラミング言語「TypeScript 3.7」のβ版を公開
Microsoft、プログラミング言語「TypeScript 3.6」を公開
PythonがJavaを追い抜く、SlashDataの開発者実数調査Copyright © ITmedia, Inc. All Rights Reserved.