TypeScript には、文字列リテラル型を大文字や小文字に変換するための組み込み型ユーティリティが4つ用意されています。Uppercase<T>・Lowercase<T>・Capitalize<T>・Uncapitalize<T> です。これらは「値」ではなく「型」を変換するもので、実行時に文字列を書き換えるわけではありません。この記事では、4つの型それぞれの基本的な使い方から、Template Literal Types や mapped types と組み合わせて onClick のようなイベント名の型を自動生成する実践例まで、動くコードとあわせて解説します。型レベルで文字列を扱うことに興味がある初級〜中級の方に向けた内容です。
目次
何ができるのか(値ではなく型を変換する)
この4つの型は、文字列リテラル型の中身を大文字化・小文字化した新しい文字列リテラル型を作り出すためのものです。TypeScript コンパイラに組み込まれた特別な型(intrinsic string manipulation types)で、自分で書けるような型ではなくコンパイラ内部で処理されます。
ここでもっとも大事なのは、これらが実行時の文字列変換ではないという点です。Uppercase<'hello'> は 'HELLO' という型を生み出しますが、プログラムを実行して 'hello' という値が 'HELLO' に変わるわけではありません。あくまで型チェックの世界だけで起きる変換です。実際の値を大文字にしたいときは、後述するように String.prototype.toUpperCase() などのメソッドを使います。
では、なぜ型だけを変換できると嬉しいのでしょうか。TypeScript には Template Literal Types という、文字列リテラル型どうしを組み立てて新しい文字列型を作る機能があります。このとき「プロパティ名の先頭だけ大文字にしたイベント名」や「大文字に統一したキー」といった型を、元の型から機械的に導き出したい場面が出てきます。そうした型レベルの文字列加工を担うのが、これら4つの型です。
Uppercase:すべて大文字にする
Uppercase<T> は、文字列リテラル型 T のすべての文字を大文字に変換した型を返します。次の例では、'hello' という型が 'HELLO' という型に変換されます。
// 文字列リテラル型 'hello' を大文字化した型になる type Result = Uppercase<'hello'>; // Result は 'HELLO' 型 // そのため 'HELLO' 以外の文字列は代入できない const greeting: Result = 'HELLO'; // OK // const ng: Result = 'hello'; // エラー: 型 '"hello"' を '"HELLO"' に割り当てられません
Result は単なる string ではなく 'HELLO' という具体的なリテラル型になります。そのため、大文字化された文字列だけを受け付ける変数として振る舞います。定数や環境変数のキーを大文字に統一したい、といった型の制約に使えます。
Lowercase:すべて小文字にする
Lowercase<T> は Uppercase<T> の逆で、すべての文字を小文字に変換した型を返します。HTTP メソッドやプロパティ名など、大文字・小文字が混在しうる文字列を小文字に正規化した型を作りたいときに使います。
// 'GET' を小文字化した型になる type Result = Lowercase<'GET'>; // Result は 'get' 型 const method: Result = 'get'; // OK // const ng: Result = 'GET'; // エラー: 型 '"GET"' を '"get"' に割り当てられません
'GET' という型が 'get' という型に変換されました。もとの文字にすでに小文字が含まれていても、大文字だけが小文字に変わり、それ以外はそのまま保たれます。
Capitalize:先頭だけ大文字にする
Capitalize<T> は、文字列リテラル型 T の先頭の1文字だけを大文字に変換し、残りはそのまま保ちます。2文字目以降は変更されない点が Uppercase との違いです。
// 先頭の 'c' だけが大文字になる type Result = Capitalize<'click'>; // Result は 'Click' 型(残りの 'lick' はそのまま) const eventName: Result = 'Click'; // OK // const ng: Result = 'CLICK'; // エラー: 全部大文字にはならない
'click' が 'Click' になりました。先頭だけを大文字にする変換は、後述するイベントハンドラー名(onClick など)や getter/setter 名(getName など)を型から組み立てるときに欠かせません。
Uncapitalize:先頭だけ小文字にする
Uncapitalize<T> は Capitalize<T> の逆で、先頭の1文字だけを小文字に変換します。パスカルケースの型名をキャメルケースのプロパティ名に変換したいときなどに使います。
// 先頭の 'U' だけが小文字になる type Result = Uncapitalize<'UserName'>; // Result は 'userName' 型(残りの 'serName' はそのまま) const propName: Result = 'userName'; // OK // const ng: Result = 'username'; // エラー: 先頭以外は変わらない
'UserName' が 'userName' になり、先頭の U だけが小文字になりました。2文字目以降の N はそのまま残ります。
4つの型を一覧で整理する
ここまでの4つの型を一覧にまとめると、どれがどの範囲を変換するのかが一目で分かります。「全体」を変換するのが Uppercase / Lowercase、「先頭1文字だけ」を変換するのが Capitalize / Uncapitalize です。
| 型 | 効果 | 例(型 → 型) |
|---|---|---|
Uppercase<T> | すべての文字を大文字にする | 'hello' → 'HELLO' |
Lowercase<T> | すべての文字を小文字にする | 'HELLO' → 'hello' |
Capitalize<T> | 先頭の1文字だけ大文字にする | 'hello' → 'Hello' |
Uncapitalize<T> | 先頭の1文字だけ小文字にする | 'Hello' → 'hello' |
いずれも入力が文字列リテラル型であれば、その中身を加工した新しい文字列リテラル型を返します。入力が string のような広い型の場合は、変換できないため string のまま返る点も覚えておくとよいでしょう。
Template Literal Types と組み合わせてイベント名を作る
これらの型が真価を発揮するのは、Template Literal Types と組み合わせたときです。Template Literal Types は、バッククォートを使って文字列リテラル型どうしを連結し、新しい文字列型を作る機能です。ここに Capitalize を挟むと、'click' から 'onClick' のようなイベントハンドラー名の型を機械的に導き出せます。
// イベント名の候補
type EventName = 'click' | 'focus' | 'change';
// 'on' + 先頭大文字のイベント名 という型を組み立てる
type HandlerName<T extends string> = `on${Capitalize<T>}`;
// 'onClick' | 'onFocus' | 'onChange' に展開される
type Handlers = HandlerName<EventName>;
const handler: Handlers = 'onClick'; // OK
// const ng: Handlers = 'onclick'; // エラー: 先頭が大文字でないと合わない
ユニオン型 EventName の各メンバーに対して Capitalize が適用され、'onClick' | 'onFocus' | 'onChange' という3つのイベント名の型が一度に生成されました。イベント名を1つ増やせば、対応するハンドラー名の型も自動で増えます。手書きで onClick のような文字列を並べる必要がなくなり、タイプミスも型で防げます。
mapped types の key remapping でプロパティ名を作り替える
さらに実践的なのが、mapped types の key remapping(as 節)と組み合わせる使い方です。オブジェクト型の各プロパティ名を Capitalize で変換しながら、get〜 という getter メソッドの型を自動生成できます。次の例では、Person の各プロパティから getName・getAge というメソッドを持つ型を導き出しています。
interface Person {
name: string;
age: number;
}
// 各プロパティ名を get + 先頭大文字 に付け替えて、getter 型を作る
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// 生成される型:
// {
// getName: () => string;
// getAge: () => number;
// }
type PersonGetters = Getters<Person>;
const api: PersonGetters = {
getName: () => 'Alice',
getAge: () => 30,
};
console.log(api.getName(), api.getAge()); // Alice 30
[K in keyof T as ...] の as 節でキーを付け替えるのが key remapping です。Capitalize<string & K> としているのは、keyof T は string | number | symbol になりうるため、string & を挟んで文字列に絞り込んでから Capitalize に渡すためです。これで name が getName、age が getAge というメソッド名に変換されました。元の型にプロパティを足せば、対応する getter も型として自動的に増えていきます。
値は変換されない(型だけの変換であることに注意)
これらの型を使うときにもっとも混同しやすいのが、冒頭でも触れた「値は変換されない」という点です。Uppercase<T> などはあくまで型レベルの変換で、実行時に文字列そのものを大文字にする働きは一切ありません。次のコードで確認してみましょう。
// これは「型」の変換であって、値は何も変わらない type Upper = Uppercase<'hello'>; // 型は 'HELLO' // 実行時に文字列を大文字にしたいなら、メソッドを使う const value = 'hello'; const upper = value.toUpperCase(); // 値が 'HELLO' になる console.log(upper); // 'HELLO'
Uppercase<'hello'> はコンパイル時に 'HELLO' という型を作りますが、コンパイル後の JavaScript には型の情報は一切残りません。実行時に文字列の中身を大文字へ変えるのは String.prototype.toUpperCase() の役目です。同様に小文字化は toLowerCase() を使います。「型を変換する Uppercase」と「値を変換する toUpperCase()」は名前が似ていますが、まったく別のレイヤーで働くものだと切り分けて覚えてください。
実務では、この2つを組み合わせる場面もあります。たとえば実行時に toUpperCase() で大文字化した値を、型のうえでも Uppercase<T> で「大文字化された型」として扱えるようにすると、値と型の両方でつじつまが合う堅牢なコードになります。とはいえ両者は連動して動くわけではないので、値の変換は必ずメソッドで明示的に行う必要があります。
まとめ
TypeScript の組み込み文字列操作型 Uppercase<T>・Lowercase<T>・Capitalize<T>・Uncapitalize<T> は、文字列リテラル型を大文字化・小文字化・先頭だけ大文字化・先頭だけ小文字化する型ユーティリティです。全体を変換するのが Uppercase / Lowercase、先頭1文字だけを変換するのが Capitalize / Uncapitalize という違いがあります。単体でも文字列リテラル型の制約に使えますが、本領を発揮するのは Template Literal Types や mapped types の key remapping と組み合わせたときで、onClick のようなイベント名や getName のような getter 名の型を、元の型から機械的に導き出せます。もっとも大事なのは、これらが値ではなく型を変換するものだという点です。実行時に文字列そのものを大文字・小文字にしたいときは toUpperCase() / toLowerCase() を使い、型の変換と値の変換を混同しないようにしましょう。