こんにちは!TypeScriptの世界へようこそ。
フロントエンドからバックエンドまで、型安全なコードを書く楽しさに魅せられている頃ではないでしょうか。
今回は、TypeScriptの関数における「引数に渡す文字列リテラル型の自動補完を効かせるテクニック」について深掘りしていきます。
「特定の決まった文字列だけを受け取らせたい。でも、それ以外の自由な文字列も許容したい……。あれ?そうするとIDEの補完が消えちゃった!?」
そんな壁にぶつかったことはありませんか?
ここをクリアすれば、TypeScriptの型システムの本質と、開発者体験(DX)を爆上げするコツがグッとつかめるようになりますよ。一緒にバッチリマスターしていきましょう!
—
1. 最初にぶ L1 壁:Union型と `string` の衝突
例えば、アプリケーションのテーマカラーを切り替える関数を作るとしましょう。
受け取れる値は `”light”` または `”dark”` のどちらかだとします。
これを素直にUnion型(和集合型)で定義すると、こうなりますよね。
type Theme = “light” | “dark”;
function setTheme(theme: Theme) {
console.log(`Theme is set to: ${theme}`);
}
// 使い方
setTheme(“light”); // OK!
setTheme(“dark”); // OK!
setTheme(“sepia”); // ❌ エラー!コンパイルできません
ここまでは完璧です。「”sepia”なんて未知のテーマは許さない!」という強い意志が型に反映されています。
しかし、現場の開発ではこう思う瞬間がやってきます。
「あ、でもユーザーが独自にカスタムテーマ名(任意の文字列)を指定できるように拡張したくなってきたぞ……?」
—
2. 陥りがちな罠:`string` をそのまま足すと補完が消える
「じゃあ、型定義に `string` を足せばいいんだね!」と、以下のように書いてしまいがちです。
// ❌ 惜しい!これでは自動補完が死んでしまいます
type Theme = “light” | “dark” | string;
function setTheme(theme: Theme) {
// …
}
このコード、コンパイルエラーにはなりません。任意の文字列を渡せるようにもなります。
しかし、VS Codeなどのエディタで `setTheme(` と打った瞬間、`”light”` や `”dark”` のサジェスト(自動補完)がパタッと消えてしまうのです。
なぜ、補完が消えてしまうのか?
TypeScriptの型システムにおいて、`string` は「すべての文字列の集合(巨大な海)」です。
一方で、`”light”` は「”light” という1点のみの要素」です。
巨大な海である `string` と `”light”` をUnion型で結合(`”light” | string`)すると、TypeScriptのコンパイラはそれを「ただの `string` 型」に縮小・吸収(Widening)してしまいます。
結果として、エディタは「あ、これ何でもいい `string` なんだな」と判断し、私たちが欲しかった文字列リテラルの補完を提示できなくなってしまうのです。
「何でも入れられる自由」と引き換えに、「最高の開発者体験(補完)」を失ってしまいました。悲しいですよね。
—
3. 解決策:`(string & {})` という魔法のハック
では、任意の文字列を許容しつつ、特定の文字列リテラルの自動補完を生かすにはどうすればよいのでしょうか?
ここで、TypeScriptの型システムをちょっとアッと驚かせるテクニックを使います。それがこちらです。
// 💡 これが最強のイディオム!
type Theme = “light” | “dark” | (string & {});
function setTheme(theme: Theme) {
console.log(`Theme is set to: ${theme}`);
}
このコードをエディタで試してみてください。
`setTheme(` と入力すると、ちゃんと `”light”` と `”dark”` が候補としてサジェストされる上に、全く関係のない任意の文字列(例: `”cyberpunk”`)もエラーなく渡すことができます!
コードの意味:なぜこれ上手くいくの?
「`string & {}` って一体なに?」って思いますよね。ここが今回の最大のハイライトです。
1. `{}`(空のオブジェクト型):
TypeScriptにおいて `{}` は、「`null` と `undefined` 以外のすべての値(プリミティブ型も含む)」を受け入れる極めて広い型を意味します。
2. `string & {}`(交差型):
「`string` 型であり、かつ `{}` であるもの」という条件になります。実質的にはただの `string` と同じ値の範囲を指します。
3. コンパイラの挙動の裏をかく:
しかし、TypeScriptの型チェッカーはこの `(string & {})` という形を見たとき、「ただの `string`」として単純化(吸収)せず、別の型として保持し続けようとします。
結果として、Union型の左側にある `”light”` や `”dark”` が飲み込まれずに残り、IDEが「お、これらは明示的な候補だな」と認識して自動補完を出し続けてくれるというわけです。コンパイラエンジニアの頭の中を覗き見しているような、美しいハックですね。
—
4. 実戦での活用シーン
このテクニックは、APIのレスポンスや、デザインシステムのコンポーネント設計で猛威を振るいます。
// 頻出するサイズ指定の例
type Size = “sm” | “md” | “lg” | (string & {});
interface ButtonProps {
// よく使う標準サイズは補完させたいけれど、
// 稀に “2rem” や “100px” のようなカスタム値を突っ込みたい時に最高に輝く
size: Size;
}
const Button = ({ size }: ButtonProps) => {
return ;
};
// 開発中は “sm”, “md”, “lg” がサジェストされる!
// でも、デザインの都合で例外的な値も怒られずに渡せる!
—
まとめ
今回は、TypeScriptの型システムにおける「柔軟性と開発者体験の両立」について解説しました。
- Union型に単なる `string` を足すと、型が縮小されて補完が消えてしまう
- `(string & {})` という交差型トリックを使うことで、補完を維持したまま任意の文字列を許容できる
ここをクリアできれば、単なる「型エラーを防ぐコード」から一歩進んで、「使う人が気持ちよくなれるAPI(型)」を設計できるようになります。
型システムの裏側の挙動を少しだけ意識して、より洗練されたTypeScriptライフを楽しんでくださいね。それでは、次の記事でお会いしましょう!バッチリマスターしていきましょう!