thumbnail

TypeScript5.3~5.0の新機能をまとめて学ぶ

TypeScript5.3からTypeScript5.0に遡って新機能をまとめています。4.9以前の内容は別記事にまとめています。まとめてある内容は文法レベルの新機能で、細かい改良及び仕様変更についてはまとめていません。ざっと新機能だけを確認するのが目的です。記事の後ろに行けば行くほど古いバージョンについて書いているため、知っているバージョンまでスクロールする必要はありません。各見出しの後には、公式ドキュメント(英語)に対応する項目を記載しております。

TypeScript 5.3

インポート属性〜import from withの使用~

項目名:『Import Attributes』

モジュールのインポート時に期待されるフォーマットを、ランタイムに提供する機能です。
import文にwithでファイル形式を指定します。

// これが JSON として解釈されることを期待しています。
import obj from "./something.json" with { type: "json" }

動的インポートでは次のように記述します。

const obj = await import("./something.json", {
    with: { type: "json" }
})

これによって、意図しないモジュール実行を防ぎ、モジュールの解釈や処理方法を最適化することで、セキュリティや効率性の向上に役立てることができます。

import typeにおける解決モードの安定サポート

項目名;『Stable Support resolution-mode in Import Types』

先ほどのインポート属性を用いて、import typesのresolution-mode属性がサポートされるようになりました。
resolution-modeとは、import typeするモジュールをNodeなどで用いられるrequire文で解決するか、ECMASciprtのモジュールであるimport文で解決するか指定するものです。

import type { TypeFromRequire } from "pkg" with {
    "resolution-mode": "require"
}
import type { TypeFromImport } from "pkg" with {
    "resolution-mode": "import"
}

その他

switch文やis文、カスタムinstanceof句による型絞り込み機能の強化など

TypeScript 5.2

using宣言による明示的リソース管理

項目名:『using Declarations and Explicit Respirce Management』

using宣言の基本〜導入の動機〜

詳しく解説するために、章を分割して説明します。
まずはusing宣言が導入された動機について解説した後、簡単な使用例と実験について解説します。

プログラムを書いていて、ネットワークを閉じたり、一時ファイルを削除する、メモリの解放をするなどのクリーンアップの作業が度々必要になります。

今回はファイルを閉じる操作について、実際のコード例を挙げて説明します。
次のように、ファイル操作を行った場合は、最後に閉じたり削除するクリーンアップの処理を書く必要があります。

import * as fs from "fs"
export function readAndWriteFile() {
    const path = "file.txt"
    const file = fs.openSync(path, "w+")
    
    // ~なんらかのファイル操作~
    // クリーンアップの処理
    fs.closeSync(file)
    fs.unlinkSync(path)
}

ただファイルを開いて閉じるだけでなく、途中でなんらかの条件を満たした場合に早期リターンが発生することもあります。

import * as fs from "fs"
export function readAndWriteFile() {
    const path = "file.txt"
    const file = fs.openSync(path, "w+")
    
    // ~なんらかのファイル操作~
    // 早期リターン
    if (something) {
        // ~なんらかのファイル操作~
        // クリーンアップの処理
        fs.closeSync(file)
        fs.unlinkSync(path)
        return
    }
    // クリーンアップの処理
    fs.closeSync(file)
    fs.unlinkSync(path)
}

この時点でいちいちクリーンアップで同じ処理を繰り返し書くという冗長で忘れやすいコードがあります。
上のコードでは例外が発生した場合にクリーンアップが正しく行われないため、try ~ finally文で囲んだほうがまだ確実です。

import * as fs from "fs"
export function readAndWriteFile() {
    const path = "file.txt"
    const file = fs.openSync(path, "w+")
    
    // ~なんらかのファイル操作~
    try {
        // 早期リターン
        if (something) {
            // ~なんらかのファイル操作~
        return
        }
    } finally {
       // クリーンアップの処理
        fs.closeSync(file)
        fs.unlinkSync(path)
    }
}

このコードでも、まだ問題があります。さらに他のリソースのクリーンアップを妨げる例外が発生する場合もあるし、そもそもtry ~ finally文の記述が冗長な感じがします。

そこで、明示的なリソース管理の出番です。
この一連のファイル操作をクラスで管理します。

using宣言の基本〜使い方〜

usingを使った明示的なリソース管理では、次のようにクラスを定義します。

class TempFile implements Disposable {
    #path: string
    #handle: number
    constructor(path: string) {
        this.#path = path
        this.#handle = fs.openSync(path, "w+")
    }
    // ~色々なメンバ~
    [Symbol.dispose]() {
        // クリーンアップの処理
        fs.closeSync(this.#handle)
        fs.unlinkSync(this.#path)   
    }
}

ポイントは次の2点です。

  • このクラスはDisposableを実装(implements)している
  • Symbol.disposeというメソッド上にクリーンアップの処理を書く

そしてこのクラスをインスタンス化するときに『using』キーワードを用います。

function doSomething() {
    using file = new TempFile("file.txt")
    // ~なんらかの処理~
    if (something) {
        // ~なんらかの処理~
        return
    }
}

usingで宣言された変数はスコープの最後でSymbol.disposeを自動で呼ぶので、早期リターンされようが上のコードでクリーンアップ処理が正しく行われます。try ~ finally文も必要ありません。

using句で宣言されたインスタンスが複数ある場合、disposeが呼ばれる順番は『先入後出し(FILO / LIFO)』です。

以上が基本となります。

エラー処理

廃棄処理中に発生したエラーは廃棄後に再スローされます。

廃棄前のロジックと廃棄中のロジックが両方エラーを出した場合のために、SuppressedErrorというErrorのサブタイプが導入されました。最後にスローされたエラーはsuppressedに、最初にスローされたエラーはerrorとして持ちます。

catch (e: any) {
    console.log(e.name); // SuppressedError
    console.log(e.message); // An error was suppressed during disposal.
    console.log(e.error.name)
    console.log(e.error.message)
    console.log(e.suppressed.name)
    console.log(e.suppressed.message)
}

非同期処理のusing

非同期処理でusingを使う場合は、次のように少し変更が必要です。

class TempFile implements AsyncDisposable {
    #path: string;
    #handle: number;
    constructor(path: string) {
        this.#path = path;
        this.#handle = fs.openSync(path, "w+");
    }
   
    async [Symbol.asyncDispose]() {
        await // ~なんらかの非同期処理~
    }
}
  • Disposable→AsyncDisposableに変更
  • Symbol.dispose→Symbol.asyncDisposeに変更

単発のクリーンアップ

上記のクラスを実装する方法は簡単なクリーンアップには大袈裟な処理かもしれません。そこで、単発のクリーンアップのために、『DisposableStack/ AsyncDisposableStack』の2種類のオブジェクトが導入されました。

使用例は以下のとおりです。

function doSomthing() {
    const path = "file.txt"
    const file = fs.openSync(path, "w+")
    using cleanup = new DisposableStack()
    cleanup.defer(() => {
        fs.closeSync(file)
        fs.unlinkSync(path)
    })
    if(something) {
        return
    }
}

defer()メソッドにクリーンアップのコールバックを渡すだけす。

もう1つのAsyncdisposableStackは、asyncの名の通りDisposableStackの非同期バージョンです。

デコレータメタデータ

項目名:『Decorator Metadata』

次のように、クラスでメタデータを簡単に利用参照できます。

type Context =
    | ClassAccessorDecoratorContext
    | ClassFieldDecoratorContext
    | ClassMethodDecoratorContext 
function setMetadata(_target: any, context: Context) {
    context.metadata[context.name] = true;
}
class SomeClass {
    @setMetadata
    hoge = 30;
    @setMetadata
    accessor fuga = "hello!";
    @setMetadata
    piyo() { }
}
const ourMetadata = SomeClass[Symbol.metadata];
console.log(JSON.stringify(ourMetadata));
// { "piyo": true, "fuga": true, "hoge": true }

メタデータはデバッグだけでなく、シリアライズやDI、Mapのキーとしてプライベートに利用できたり、応用が効きます。

名前付きタプルと匿名タプルの併用

項目名:『Named and Anonymous Tuple Elements』

以前は名前付きのタプルと普通のタプルを併用できなかったが、5.2からは利用可能になりました。

type Tuple<T> = [first: T, second: T, ...T[]]

以前ならば、全てに名前をつけないか、最後の要素…T[]にも名前をつける必要がありました。

配列の共用体のエラー解消

項目名:『Easier Method Usage for Unions of Arrays』

次のような配列の共用体に対して、メソッドを呼び出してもエラーが出なくなりました。

declare let array: string[] | number[]
array.filter(x => !!x)

A[] | B[]を(A | B)[]と解釈してからfilter等メソッドを呼ぶようになっています。

その他

デフォルトでimport typeで.ts、.mts、.cts、.tsxファイル拡張子を使用するインポート型ステートメントを記述できるようになりました。他にも、機能改善が行われています。

TypeScript 5.1

return文が無い関数と戻り値undefined

項目名:『Easier Implicit Returns for undefined – Returning Functions』

今までのTypescriptでは、return文を持たない関数は暗黙的にvoidを返し、明示的にも戻り値としてvoidかanyしか指定できませんでした。つまり、return文を持たない関数からundefinedを返すことが不可能だったわけです。

function f1() {} //戻り値はvoid型
function f2(): void {}
function f3(): any {}
function f4(): undefined {} //エラー

undefinedを返すにはreturn文が必要でした。

function f4(): undefined { return }  //OK

5.1ではこのreturnの記述が不要になり、次のコードが通ります。

function f4(): undefined {} //OK

これによる恩恵として、undefinedを返す関数を引数とする高階関数に、return文のない関数を渡せるようになりました。

declare function g(f: () => undefined) : any
// return文がなくても、きちんとundefinedを返すと解釈してくれる
g(() => {}) 

以前は、return文がないためにundefinedと解釈してくれずエラーを吐いていました。

クラスのゲッタとセッタの型を無関係に定義できる

項目名:『Unrelated Types for Getters and Setters』

ゲッタの戻り値の型とセッタの引数の型は元々は同じ型しか指定できませんでしたが、TypeScript4.3から、ゲッタの戻り値の型は、セッタの戻り値の型に割り当て可能ならば、異なる型を定義できるようになりました。

5.1ではこの制約がなくなり、セッタに割り当て可能でない型もゲッタで定義できるようになりました。
つまり、次のような記述が可能となりました。

interface Serializer {
    set value(v: string | number | boolean);
    get value(): string | undefined;
}

その他

JSXの型推論の改善、名前付きJSX属性など

TypeScript 5.0

デコレータ

項目名:『Decorators』

以前から実験的にデコレータは使えましたが、正式採用にあたり書式も違い互換性が切れています。

デコレータとはECMAScriptの新機能で、クラスのメンバを「@デコレータ名」で修飾し、再利用可能な機能を付与します。といってもよく分からないと思うので、具体例を挙げて説明します。

次のようなクラスを考えます。

class Person {
    constructor(
        private name: string
    ){}
    sayHello() {
        console.log(`Hello ${this.name}`)
    }
    sayGoodbye() {
        console.log(`Goodbye ${this.name}`)
    }
}

各メソッドの出力前後に、ログを表示するとします。

class Person {
    constructor(
        private name: string
    ){}
    sayHello() {
        console.log("LOG:メソッド呼び出し")
        console.log(`Hello ${this.name}`)
        console.log("LOG:メソッド終了")
    }
    sayGoodbye() {
        console.log("LOG:メソッド呼び出し")
        console.log(`Goodbye ${this.name}`)
        console.log("LOG:メソッド終了")
    }
}

メソッド本文前後に同じ記述があり冗長なコードです。デコレータを使えば、この同じ処理をまとめて綺麗に記述できます。

次のような高階関数をデコレータとして定義します。

function log(originalMethod: any, context: ClassMethodDecoratorContext) {
    function replacementMethod(this: any, ...args: any[]) {
        console.log("LOG:メソッド呼び出し")
        const result = originalMethod.call(this, ...args)
        console.log("LOG:メソッド終了")
        return result
    }
    return replacementMethod
}

関数の第一引数にはデコレータをつけるためのメソッドが渡り、第二引数のcontextにはメソッド名などのメタデータが渡されます。型はClassMethodDecoratorContextが用意されています。

そして、次のように修飾します。

class Person {
    constructor(
        private name: string
    ){}
    @log
    sayHello() {
        console.log(`Hello ${this.name}`)
    }
    @log
    sayGoodbye() {
        console.log(`Goodbye ${this.name}`)
    }
}

以上が基本となります。
公式ドキュメントには、メタデータを利用する例、2重のデコレータや引数付きデコレータの例もあるので、適宜参考にしてください。

const型パラメータ

項目名:『const Type Parameters』

型パラメータにconstを指定できるようになりました。

<const T extends readonly string[]>

これによってどういう変化があるのか解説します。
constが指定できない頃は、次のコードのように、より一般的なstring[]型が推測されていました。

function getNames<T extends readonly string[]>(arg: T): T {
    return arg
}
const names1 = getNames(["Taro", "Jiro", "Hanako"]) //string[]型

constをつけることによって、as const指定したのと同じような狭いリテラル型として推論されます。

function getNamesExactly<const T extends readonly string[]>(arg: T): T {
    return arg
}
// readonly ["Taro", "Jiro", "Hanako"]型
const names2 = getNamesExactly(["Taro", "Jiro", "Hanako"])

注意点として、readonlyをつけないと、依然としてstring[]型で推論されてしまいます。

その他

enum型やtsconfig.jsonファイルの記述に改善が加えられました。

TypeScript 4.x

参考

公式ドキュメント