これは ドリコムアドベントカレンダー2024 の18日目の記事です。

こんにちは、SREソリューション部の青木です。
今年は秋が短くて「食欲の秋だから」という言い訳が使えなくて少し寂しさを覚えています。

今回のお話

さて、今回はTypeScriptに慣れてきて「型って大事だな!」「TypeScript便利!」となってきた人にお送りする
TypeScriptの個人的おもしろ型指定 5選
について書きたいと思います!
TypeScriptには、他の言語にはない独特な型指定の機能が備わっており、見ていてとても興味深かったので、中でも面白いと思った5つをご紹介します。

もちろん、TypeScriptに慣れてない方、これから触ってみたいという方にも
面白い内容だと思いますのでぜひ読んでみてください。

おもしろ型指定 5選

1つ目: ReturnType<Type>

まずはReturnTypeです。
これは関数の戻り値についての型を取得する時に使えます。

function toFormattedString(target: number | Date, format: string): string {
  let result: string = '';
  // 中略
  return result;
}

type FormattedString = ReturnType<typeof toFormattedString>;
// toFormattedString の戻り値はstring なので
// type FormattedString = string と同様

多くの場合は関数が返した結果は変数に入れてしまうのであまりこの指定は使わないかもしれません。
関数の戻り値を他の関数の引数として使うような場合には関数定義部分でお世話になることはありそうですね。

2つ目 Parameters<Type>

関数の戻り値から型指定できるなら引数からでもできるよね?!
ということで2つ目はParametersです!
こちらは関数の引数をタプル型として取得することができます。

function toFormattedString(target: number | Date, format: string): string {
  let result: string = '';
  // 中略
  return result;
}

// タプルなので 0 を指定して最初の引数を取得
type FormatTarget = Parameters<typeof toFormattedString>[0]
// 最初の引数(target)は number | Date なので
// type FormatTarget = number | Date と同様

Parameters<Type>は引数の準備をしたい場合に便利です。
同じモジュール内の関数であれば引数用の型を用意すれば良いだけですが、
別モジュールにあって関数の定義に手を出せないなどの理由があるときには
Parameters<Type>が活躍します。

具体例として、上記で挙げたtoFormattedString 関数に渡す引数を通信やファイルなど外部から受け取る場合を考えてみます。

import { toFormattedString } from 'format-string';

type FormatTarget = Parameters<typeof toFormattedString>[0]

function recieveFormatTarget(): FormatTarget {
    // 通信やファイルから対象を読み込む
    return target;
}

こうしておけば、仮に toFormattedString 側の引数が拡張されてもrecieveFormatTargetの中を調整するだけで済みます。
他にもtoFormattedStringやrecieveFormatTargetを呼び出す箇所で型を指定していれば全てを修正して回るところですが、Parametersを使えばその必要はなくなります!

3つ目 Literal Types

これはご存知の方も多いかもしれません。
TypeScriptでは数値や文字列などのリテラルをそのまま型として定義できます。

type Weather = 'sunny' | 'cloudy' | 'rainy' | 'snowy';

実際の使い方としては他の言語でいうところの「enum」に近く、
下記のようにMapのキーにすると誤字を含む予期せぬキーの追加や参照を防げます。

type Weather = 'sunny' | 'cloudy' | 'rainy' | 'snowy';

const weatherInfo = new Map<Weather, string>();
// これはコンパイルOK
const cloudyInfo = weatherInfo.get('cloudy');
// これはコンパイルエラー
const cloudyInfo = weatherInfo.get('clody');

4つ目 Template Literal Types

3つ目に続いてLiteral Typesの延長であるTemplate Literal Typesです!
これを知ったのはまさに今年に入ってからのことでした。
Literal Typesでは固定の文字列や数値だけでしたが、これを使えばより柔軟な指定ができます。
なかなかスパっと言い表す良い言葉が見つからないので実際のコードで見てみましょう。

type Lang = 'ja' | 'en' | 'fr';
type TextFile = `${string}_${Lang}`;

const fileMap = new Map<TextFile, string>();

さて、これでfileMapのキーにできる文字列は想像できたでしょうか?
なんと、任意の文字列で末尾が「_ja」か「_en」か「_fr」で終わるものです。
つまり、「${string}」と指定した部分には好きな文字列が、「${Lang}」と指定した部分にはLangで指定した文字列が入れられるようになります。
簡単に言うと、型指定に従ったフォーマットの文字列だけが入るようになる、という感じでしょうか。
これは画期的ですね!他の言語ならstringとして定義して、使うときに都度フォーマットに従っているかをチェックしているところですが、そもそもこの型指定なら他の文字列は入りません。(型アサーションなどで無理やり変えない限り、ですが…)
まさに面白い型指定の機能です!

5つ目 Indexed Access Types

5つ目はIndexed Access Typesです!これは、型のメンバの型を取得する際に便利です。

// ユーザー
type User = {
  id: number,
  name: string,
  lastLogin: Date,
}

type UserNameType = User['name'];

これでUserのnameの型であるstringが取れます。
筆者はIndedxed Access Types は型の定義を調べにいくのが大変な時によく使います。
例えば下記のような型があったとします。

// 作品情報
type Movie = {
  id: number,
  // 名称
  name: string,
};

// キャラクター
type Character = {
  id: number,
  name: string,
  // 登場作品
  appearanceMovie: Movie,
};

// ユーザー
type User = {
  id: number,
  name: string,
  lastLogin: Date,
  // 好きなキャラクター
  favoriteCharacter: Character,
};

// ユーザーの好きなキャラクターが登場する作品の情報を取得
function getFavoriteCharacterMovie(user: User): Movie {
  return user.favoriteCharacter.appearanceMovie;
}

上記の関数定義、各型の定義が1つのファイルに集まっている場合には特に問題になりません。
しかし、たいていのプロジェクトでは全て別々のファイルに記載がされていることも多いと思います。
そんな時にgetFavoriteCharacterMovie関数の戻り値を書くとすると、Userの定義からCharacterの定義へジャンプし、appearanceMovieの型を見る必要があります。しかし、そんなことをしなくても

// ユーザーの好きなキャラクターが登場する作品の情報を取得
function getFavoriteCharacterMovie(user: User) : User['favoriteCharacter']['appearanceMovie'] {
  return user.favoriteCharacter.appearanceMovie;
}

という指定をすることで、appearanceMovieの実際の型が何であるかを知る必要はなくなります。
また、IDEの力を使えばUserから名前を補完して辿って行くこともできるので、定義ジャンプを繰り返すことなく戻り値の型を指定できます。
定義ジャンプを繰り返すと、作成中のファイルに戻るのも手間になるのでこれは楽ですね!

終わりに

皆さん、いかがでしたか?
TypeScriptの型指定には他の言語に比べて面白い方法がありますね!
これで少しでもTypeScriptとその型指定に興味を持っていただけたなら嬉しいです。

※ 記事中のコードはTypeScript: TS Playgroundでバージョンをv5.7.2にした時にエラーが出ないことを確認しています。バージョンによっては使えない機能や変更されている可能性がありますのでご注意ください。

参考文献

TypeScript DocumentationCreating Types from Types および Utility Types