2023年3月16日にTypeScript 5.0が正式にリリースされ、誰でも利用できるようになりました。このリリースでは、TypeScriptをよりコンパクトに、よりシンプルに、そしてより高速にすることを目的として、多くの新しい機能が導入されています。

クラスカスタマイズのデコレータで顕著な改良が見られ、クラスとそのメンバーを再利用可能なかたちでカスタマイズできるようになりました。開発者に嬉しい情報として、型パラメータ宣言にconst修飾子を追加でき、constライクな推論をデフォルトで実行可能です。また、今回のリリースでenum(列挙型)に大きな変更が加えられ、コード構造の簡素化とTypeScript全体の高速化も実現しています。

この記事では、TypeScript 5.0で導入された変更点を探り、その新しい機能や特徴を詳しくご紹介します。

TypeScript 5.0の利用を始める

TypeScriptは、npmを使ってプロジェクトにインストールすることができる公式のコンパイラです。プロジェクトでTypeScript 5.0を使い始めるには、プロジェクトのディレクトリで次のコマンドを実行します。

npm install -D typescript

これにより、コンパイラがnode_modulesディレクトリにインストールされ、npx tscコマンドで実行できるようになります。

また、Visual Studio Codeでより新しいバージョンのTypeScriptを使用する手順もこのドキュメントに記載されています。

TypeScript 5.0で何が新しくなったのか

TypeScriptに導入された5つの主要な変更内容をご紹介します。以下の通りです。

デコレータ

デコレータは、TypeScriptでは実験的なフラグとしてしばらく存在していましたが、今回の新しいリリースでは、ECMAScriptの提案に対応しステージ3、つまりTypeScriptに追加される段階になっています。

デコレータを使えば、クラスとそのメンバーの動作を再利用可能な方法でカスタマイズできます。例えば、greetgetAgeという2つのメソッドを持つクラスがあるとします。

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }

    getAge() {
        console.log(`I am ${this.age} years old.`);
    }
}

const p = new Person('Ron', 30);
p.greet();
p.getAge();

実際の使用例では、このクラスには非同期ロジックを扱う複雑なメソッドがあったり、副作用が介在したり(メソッドのデバッグを助けるためにconsole.logを呼び出す等)するはずです。

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet() {
        console.log('LOG: Method Execution Starts.');
        console.log(`Hello, my name is ${this.name}.`);
        console.log('LOG: Method Execution Ends.');
    }

    getAge() {
        console.log('LOG: Method Execution Starts.');
        console.log(`I am ${this.age} years old.`);
        console.log('Method Execution Ends.');
    }
}

const p = new Person('Ron', 30);
p.greet();
p.getAge();

これは頻発するパターンなので、どの方法にも適用できる解決策があると便利です。

ここでデコレータの出番です。次のように表示されるdebugMethodという名前の関数を定義することができます。

function debugMethod(originalMethod: any, context: any) {
    function replacementMethod(this: any, ...args: any[]) {
        console.log('Method Execution Starts.');
        const result = originalMethod.call(this, ...args);
        console.log('Method Execution Ends.');
        return result;
    }
    return replacementMethod;
}

上のコードでは、debugMethodがオリジナルメソッド(originalMethod)を受け取り、次のような関数を返しています。

  1. 「Method Execution Starts」というメッセージをログに出力
  2. オリジナルメソッドとそのすべての引数(thisを含む)を渡す
  3. 「Method Execution Ends」というメッセージをログに出力
  4. オリジナルメソッドからのものをすべて返す

デコレータを使用することで、以下のコードのようにメソッドにdebugMethodを適用することができます。

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    @debugMethod
    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }
    @debugMethod
    getAge() {
        console.log(`I am ${this.age} years old.`);
    }
}
const p = new Person('Ron', 30);
p.greet();
p.getAge();

これにより、以下のような出力が得られます。

LOG: Entering method.
Hello, my name is Ron.
LOG: Exiting method.
LOG: Entering method.
I am 30 years old.
LOG: Exiting method.

デコレータ関数(debugMethod)を定義するとき、contextという2番目のパラメータが渡されます(これはコンテキストオブジェクトで、メソッドがどのように宣言されたか、またメソッドの名前など、便利な情報となります)。debugMethodを更新し、contextオブジェクトからメソッド名を取得することができます。

function debugMethod(
    originalMethod: any,
    context: ClassMethodDecoratorContext
) {
    const methodName = String(context.name);
    function replacementMethod(this: any, ...args: any[]) {
        console.log(`'${methodName}' Execution Starts.`);
        const result = originalMethod.call(this, ...args);
        console.log(`'${methodName}' Execution Ends.`);
        return result;
    }
    return replacementMethod;
}

コードを実行すると、debugMethodデコレータで装飾された各メソッドの名前が出力されます。

'greet' Execution Starts.
Hello, my name is Ron.
'greet' Execution Ends.
'getAge' Execution Starts.
I am 30 years old.
'getAge' Execution Ends.

デコレータでできることはまだまだあります。TypeScriptでデコレータを使う方法については、元のプルリクエストをご覧ください。

const型パラメータの導入

これは、関数を呼び出すときに得られる推論を改善するもので、ジェネリック型までその対象範囲が拡大しています。デフォルトでは、constで値を宣言すると、TypeScriptにより(リテラル値ではなく)型が推論されます。

// Inferred type: string[]
const names = ['John', 'Jake', 'Jack'];

これまでは、目的どおりの推論を実現するためには、「as const」を付けてconstアサーションを行う必要がありました。

// Inferred type: readonly ["John", "Jake", "Jack"]
const names = ['John', 'Jake', 'Jack'] as const;

関数を呼び出すときも、似たようになります。以下のコードでは、推論されるcountries(国)の種類はstring[] です。

type HasCountries = { countries: readonly string[] };
function getCountriesExactly(arg: T): T['countries'] {
    return arg.countries;
}

// Inferred type: string[]
const countries = getCountriesExactly({ countries: ['USA', 'Canada', 'India'] });

そして、より正確な型を求めるのであれば(以前から可能な方法として)as constアサーションを使うことができます。

// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] } as const);

とは言え、これは覚えるのも実装するのも時に面倒になり得ます。そこで、TypeScript 5.0では、型パラメータの宣言にconst修飾子を追加する機能が導入されました。これにより、デフォルトで自動的にconstライクな推論を適用することができます。

type HasCountries = { countries: readonly string[] };
function getNamesExactly(arg: T): T['countries'] {
    return arg.countries;
}

// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] });

const型パラメータを使用することで、開発者はコード内で今まで以上に明確に意図を表現することができます。ある変数が一定で変化しないことを意図する場合、const型パラメータを使用することで、誤って変化してしまう可能性を排除できます。

TypeScriptでconst型パラメータがどのように機能するかについては、元のプルリクエストをご確認ください。

enumの改良

TypeScriptのenumを使用すれば、名前付き定数のまとまりを柔軟に定義することができます。TypeScript 5.0では、このenumに改良が加えられ、より便利なものになりました。

例えば、ある関数に次のようなenumが渡されたとします。

enum Color {
    Red,
    Green,
    Blue,
}

function getColorName(colorLevel: Color) {
    return colorLevel;
}

console.log(getColorName(1));

TypeScript 5.0が導入される前は、間違った数字を渡しても、エラーは出ませんでした。しかし、TypeScript 5.0の導入により、すぐにエラーが投げられるようになりました。

また、今回のリリースでは、メンバーごとにユニークな型を作成することで、enumがunion enumになって(enumの値に定数以外の値を指定可能)います。この改善により、すべてのenumから絞り込み、そのメンバーを型として参照することができるようになりました。

enum Color {
    Red,
    Purple,
    Orange,
    Green,
    Blue,
    Black,
    White,
}

type PrimaryColor = Color.Red | Color.Green | Color.Blue;

function isPrimaryColor(c: Color): c is PrimaryColor {
    return c === Color.Red || c === Color.Green || c === Color.Blue;
}

console.log(isPrimaryColor(Color.White)); // Outputs: false
console.log(isPrimaryColor(Color.Red)); // Outputs: true

TypeScript 5.0のパフォーマンス向上

TypeScript 5.0では、コードの構造、データの構造、アルゴリズムの拡張など、数多くの重要な変更が行われています。これにより、インストールから実行まで、TypeScriptの使いやすさ全体が改善され、より速く、より効率的になりました。

例えば、TypeScript 5.0と4.9のパッケージサイズの差は、非常に大きなものになっています。

TypeScriptは最近、名前空間からモジュールに移行され、スコープのホイスティング(巻き上げ)などの最適化を行うことができるようになりました。また、非推奨コードの削除により、TypeScript 4.9のパッケージサイズ(63.8MB)の約26.4MB削減に成功しています。

TypeScriptのパッケージサイズ
TypeScriptのパッケージサイズ

TypeScript 5.0と4.9の間で、スピードとサイズにおいてさらに興味深い点をいくつか以下にご紹介します。

項目 TS 4.9に比較した時間やサイズ
マテリアルUIビルド時間 90%
TypeScript Compilerの起動時間 89%
Playwrightのビルド時間 88%
TypeScript Compilerのセルフビルド時間 87%
Outlook Webのビルド時間 82%
VS Codeのビルド時間 80%
typescript npmパッケージサイズ 59%

バンドラー(モジュール解決の改善)

TypeScriptでimport文を書くとき、コンパイラはimportが何を指しているかを把握する必要があります。これを実現するのが、モジュール解決です。例えば、import { a } from "moduleA"と書くと、コンパイラはmoduleAaの定義を理解する必要があります。

TypeScript 4.7 では、--modulemoduleResolutionの設定に、node16nodenextという2つの新しいオプションが追加されました。

これらのオプションの目的は、Node.jsのECMAScriptモジュールの正確なルックアップルールをより正確に表現することでした。しかし、このモードには、他のツールでは強制されない制約が複数あります。

例えば、Node.jsのECMAScriptモジュールでは、相対的なインポートが正しく動作するためには、ファイル拡張子を明確にする必要があります。

import * as utils from "./utils"; // Wrong 

import * as utils from "./utils.mjs"; // Correct

「moduleResolution bundler」という新しい手法が導入されました。これは、TypeScript設定ファイルの「compilerOptions」セクションに以下のコードを追加することで実装することができます。

{
    "compilerOptions": {
        "target": "esnext",
        "moduleResolution": "bundler"
    }
}

この手法は、Vite、esbuild、swc、Webpack、Parcelなど、ハイブリッドルックアップを用いる最新のバンドラーを使用している人に適しています。

TypeScriptでmoduleResolutionがどのように動作するかについては、元のプルリクエストとその実装についての説明をご確認ください。

非推奨事項

TypeScript 5.0には、ランタイムの要件、lib.d.tsの変更、APIの変更など、いくつかの非推奨事項があります。

  1. ランタイム:TypeScriptは現在、ECMAScript 2018をターゲットとしており、パッケージではエンジンの期待値として12.20を最低限の条件と定めています。したがって、Node.js利用者は、TypeScript 5.0を使用するために最低でも12.20以降を使用する必要があります。
  2. lib.d.ts:DOMの型の生成方法にいくつか変更があり、既存のコードに影響を与える可能性があります。特に、特定のプロパティが数値から数値リテラル型に変換され、カット、コピー、ペーストのイベント処理プロパティとメソッドがインターフェースを越えて移動しています。
  3. API:一部の不要なインターフェースが削除され、複数の改善策が施されました。また、TypeScript 5.0では、モジュールへの移行も行われています。

TypeScript 5.0では、target: ES3, out, noImplicitUseStrict, keyofStringsOnly, suppressExcessPropertyErrors, suppressImplicitAnyIndexErrors, noStrictGenericChecks, charset, importsNotUsedAsValues, preserveValueImportsを含む特定の設定とそれに対応する値、およびプロジェクト参照におけるprependが非推奨となりました。

これらの設定はTypeScript 5.5まで有効ですが、まだ使用しているユーザーには注意喚起のための警告が出されます。

まとめ

今回は、TypeScript 5.0がもたらす主な機能・改善点として、enum、バンドラー、const型パラメータや、速度・サイズの改善などについてご紹介しました。

次のプロジェクトでのTypeScriptの利用を考えている方は、是非ともKinstaのアプリケーションホスティングをお試しください。

TypeScript 5.0で最も魅力的だと思う機能や改善点は何ですか?また、この記事で見落とした重要な点はありますか?コメント欄でお聞かせください。

Joel Olawanle Kinsta

Kinstaでテクニカルエディターとして働くフロントエンド開発者。オープンソースをこよなく愛する講師でもあり、JavaScriptとそのフレームワークを中心に200件以上の技術記事を執筆している。