1月30日、MicrosoftはTypeScript 5.8 ベータ版をリリースした。
この記事では、TypeScript 5.8ベータ版の新機能や変更点について紹介する。
本記事は、以下のエキスパートに監修していただきました:
条件付き型やインデックス型の戻り値への型チェック
以下のように、条件付き型やインデックス型を用いると、パラメータの分岐条件に応じて返り値の型を厳密化できる。たとえば、一つの要素だけを選択するか、複数を選択するかで返り値の型を切り替える関数があるとする。
/**
* @param prompt ユーザーに表示するテキスト
* @param selectionKind ユーザーが選択できるオプションが複数か単数か
* @param items ユーザーに表示されるオプション
**/
async function showQuickPick(
prompt: string,
selectionKind: SelectionKind,
items: readonly string[],
): Promise<string | string[]> {
// ...
}
enum SelectionKind {
Single,
Multiple,
}
単純な定義では呼び出し側が返り値を厳密に推測できず、型エラーを回避するためのチェックなどが必要になる。以下の例では、配列として扱いたいところが文字列か配列かを曖昧なまま扱うためにエラーが発生する。
let shoppingList = await showQuickPick(
"Which fruits do you want to purchase?",
SelectionKind.Multiple,
["apples", "oranges", "bananas", "durian"],
);
console.log(`Alright, going out to buy some ${shoppingList.join(", ")}`);
// ~~~~
// エラー!
// プロパティ 'join' は型 'string | string[]' に存在しません
// プロパティ 'join' は型 'string' に存在しません
そこで条件付き型を使うと、返り値の型を呼び出し時のパラメータに応じてより厳密に指定できる。
type QuickPickReturn<S extends SelectionKind> =
S extends SelectionKind.Multiple ? string[] : string
// selectionKindの値に応じて戻り値の型が決定する
async function showQuickPick<S extends SelectionKind>(
prompt: string,
selectionKind: S,
items: readonly string[],
): Promise<QuickPickReturn<S>> {
// ...
}
これにより、呼び出し側では以下のように期待どおりの型を推論できる。
let shoppingList: string[] = await showQuickPick(
"Which fruits do you want to purchase?",
SelectionKind.Multiple,
["apples", "oranges", "bananas", "durian"],
);
let dinner: string = await showQuickPick(
"What's for dinner tonight?",
SelectionKind.Single,
["sushi", "pasta", "tacos", "ugh I'm too hungry to think, whatever you want"],
);
しかし、 showQuickPick()
関数を実際に実装しようとすると、期待通りに動作しない。
async function showQuickPick<S extends SelectionKind>(
prompt: string,
selectionKind: S,
items: readonly string[],
): Promise<QuickPickReturn<S>> {
if (items.length < 1) {
throw new Error("At least one item must be provided.");
}
// すべてのオプションに対するボタンを生成する
let buttons = items.map(item => ({
selected: false,
text: item,
}));
// 必要に応じ、最初の要素をデフォルトで選択状態に
if (selectionKind === SelectionKind.Single) {
buttons[0].selected = true;
}
// イベントハンドリングのコードをここに記述...
// 選択されているオプションを抽出
const selectedItems = buttons
.filter(button => button.selected)
.map(button => button.text);
if (selectionKind === SelectionKind.Single) {
// 最初に選択されたオプションのみを返す
return selectedItems[0];
}
else {
// 選択されたすべてのオプションを返す
return selectedItems;
}
}
このコードをコンパイルしようとすると、各 return
ステートメントでエラーが発生してしまう。
Type 'string[]' is not assignable to type 'QuickPickReturn<S>'.
Type 'string' is not assignable to type 'QuickPickReturn<S>'.
これまでTypeScript では、条件型を返す関数を実装するために型アサーションが必要だった。
if (selectionKind === SelectionKind.Single) {
// Pick the first (only) selected item.
- return selectedItems[0];
+ return selectedItems[0] as QuickPickReturn<S>;
}
else {
// Return all selected items.
- return selectedItems;
+ return selectedItems as QuickPickReturn<S>;
}
しかし型アサーションは、TypeScript が本来実行する正当な型チェックを無効にしてしまうため、理想的な動作とは言えない。
この型アサーションを回避するために、TypeScript 5.8 では 戻り値の条件型に対する限定的な形式のチェックがサポートされる ようになった。
戻り値の型がジェネリクスを用いた条件型の場合、その型のジェネリックパラメーターに対して制御フロー分析を使用し、より絞り込んだ型で判定を行う。
この機能を利用するには、 絞り込みが各分岐でどのように機能するかを、より明確かつ網羅的に記述する必要がある。
// SelectionKindの値に応じて型を絞り込む(neverの記述も必要)
type QuickPickReturn<S extends SelectionKind> =
S extends SelectionKind.Multiple ? string[] :
S extends SelectionKind.Single ? string :
never;
このコードを実行すると、先程のサンプルコードが型アサーション無しで機能するようになるまた、ifによる分岐の内容を入れ替えようとすると、TypeScript はそれをエラーとして正しく扱うことができる。
if (selectionKind === SelectionKind.Single) {
// Oops! Returning an array when the caller expects a single item!
return selectedItems;
// ~~~~~~
// error! Type 'string[]' is not assignable to type 'string'.
}
else {
// Oops! Returning a single item when the caller expects an array!
return selectedItems[0];
// ~~~~~~
// error! Type 'string[]' is not assignable to type 'string'.
}
さらに、インデックス型での分岐にも対応しており、以下のようにSelectionKind
をキーに持つマッピング型を定義する方法でも同様の型安全性を実現できる。
interface QuickPickReturn {
[SelectionKind.Single]: string;
[SelectionKind.Multiple]: string[];
}
async function showQuickPick<S extends SelectionKind>(
prompt: string,
selectionKind: S,
items: readonly string[],
): Promise<QuickPickReturn[S]> {
// ...
}
現状、この仕組みは単一のジェネリックパラメータに限定されていたり、条件付き型で使用する場合は2つ以上のチェックと、分岐の最後にneverを指定する必要があるなど、いくつかの制約がある。しかし多くのユースケースで、型アサーションを使わずに複雑な条件分岐に対応できるようになった点は注目に値する。
require()
を使ったESM読み込みへの対応(--module nodenext
)
Node.js 22では、CommonJSモジュールからECMAScriptモジュール(ESM)をrequire()
できるようになった(ただし、トップレベルでawait
を含むESMファイルは依然としてrequire()
できない)。
これに伴い、TypeScript 5.8では--module nodenext
フラグを使うことで、Node.js 22以降でのrequire("esm")
を許容するようになっている。
- ESMからCommonJSへの読み込み(
import
)は以前から可能 - CommonJSからESMへの読み込み(
require()
)も、Node.js 22以降ではほぼ可能に
今後は--module node18
や--module node20
といったバージョン依存の設定が強化される見込みであるが、現時点ではNode.js 22を利用する場合、--module nodenext
の使用が推奨されている。
--module node18
の追加
TypeScript 5.8では、--module node18
フラグが新たに導入された。これはNode.js 18向けに安定動作するモードであり、nodenext
に比べると以下のような差異がある。
- CommonJSからのESM読み込み(
require()
)は許可されない - import assertions(後述のimport attributesに置き換えられる機能)は許可される
--erasableSyntaxOnly
オプション
Node.js 23.6以降で実装された「TypeScriptファイルを直接実行する機能」では、型情報などのTypeScript固有の部分が完全に「消せる」(erasable)ものでなければならない。たとえば以下のような構文はサポート外となる。
enum
宣言- ランタイムコードを含む
namespace
/module
- クラスのパラメータプロパティ
import
エイリアス
TypeScript 5.8では、--erasableSyntaxOnly
を指定すると、これらの対応外構文が使われた場合にエラーとして検出される。以下はパラメータプロパティを使った例だが、erasableSyntaxOnly
を有効にするとエラーになる。
class C {
constructor(public x: number) {
// error! This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
}
}
--libReplacement
フラグ
TypeScript 4.5から、@typescript/lib-*
というパッケージを通じてデフォルトのlib
ファイルを置き換える機能が存在していた。しかし、この機能を使わない場合でも、TypeScriptが常にnode_modules
を監視して置き換えパッケージを探すためのオーバーヘッドが発生していた。TypeScript 5.8では、--libReplacement
をfalse
に設定することで、この仕組みを無効化できるようになった。将来的には--libReplacement false
がデフォルトになる可能性がある。
宣言ファイルでの計算型プロパティ名の保持
TypeScript 5.8では、クラス内の計算型プロパティ([propName] = ...
など)の扱いが改善され、宣言ファイル(.d.ts)においてもプロパティ名が正しく保持されるようになった。たとえば、以下のコードは以前のバージョンではエラーになり、生成された.d.tsもインデックスシグネチャに変換されていた。
export let propName = "theAnswer";
export class MyClass {
[propName] = 42;
}
TypeScript 5.8ではエラーが出ず、宣言ファイルでも同じプロパティ名が保たれるようになる。ただしリテラル型などを使わない限り、出力された型定義ではインデックスシグネチャ相当の動作になり、静的なプロパティ名として確定するわけではない点に留意が必要だ。
まとめ
TypeScript 5.8のリリース候補版は数週間以内に公開され、その後安定版がリリースされる予定とされている。最新動向や詳細な日程については、公式のイテレーションプランを参照していただきたい。
詳細は公式のリリースノートを参照していただきたい。