🎄

intrinsicとTemplate Literal Types / TypeScript一人カレンダー

2022/12/23に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の16日目です。昨日は『Record<K, T>』を紹介しました。

intrinsic

ここまで連日Utility Typesの中身を紹介してきましたが、それらをまとめたファイルがlib.es5.d.tsです。そしてUtility Typesをいくつか読み進めていくとintrinsicというワードを見かけることになります。

intrinsicとはどういった単語でしょう、筆者が業務上で頻繁に扱う単語ではありません。そこでいくつか調べてみました。

リーダーズ英和 第3版
in・trin・sic
形容詞 本来備わっている, 固有の, 内在的な, 本質的な

ランダムハウス英和 第2版
in・trin・sic
形容詞
1 本来備わっている, 固有の, 本質的な
ESSENTIAL 類語

ということで「コンパイラ内部で処理している」といった解釈ができそうです。この前提をもとにTypeScriptのドキュメントを確認すると、Intrinsic String Manipulation Typesという記述が確認できます。

These types come built-in to the compiler for performance and can’t be found in the .d.ts files included with TypeScript.

つまり"built-in to the compiler for performance"のニュアンスがintrinsicというワードでまとめられているようです。

intrinsicというワードが割り当てられたUtility Typesについては、これまでのように実装を確認することができません。つまりMapped Types, Conditional Typesの組み合わせで模倣できたこれまでの型とは異なり、コンパイラの内部処理として実装されていることから、コンパイラ内部の実装を直接確認するしかないのです。

そのためintrinsicというワードが割り当てられた型は、intrinsicだからといって同じ振る舞いをするわけではなく、その型名によって振る舞いが異なる点に注意が必要です。

Intrinsic String Manipulation Types

TypeScript 4.9の現在、intrinsicが割り当てられたUtility Typesは4つあります。

これらは型パラメータとして文字列を満たすSをとります。型名からある程度結果が予想しやすいですが、挙動を確認してみましょう。今回のサンプルコードはすべてドキュメントからの引用です。

type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
//   ^? "HELLO, WORLD"

type QuietGreeting = Lowercase<Greeting>
//   ^? "hello, world"

type LowercaseGreeting = "hello, world";
type Greeting2 = Capitalize<LowercaseGreeting>;
//   ^? "Hello, world"

type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
//   ^? "hELLO WORLD"

このように、すべて大文字にする、すべて小文字にする、先頭を大文字にする、先頭を小文字にするという結果を確認できます。ではこれをいつ使うのかというと、Template Literal Typesとの組み合わせです。

https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#handbook-content

この要素はさり気なく以前『Mapped Typesを活用する』の回で触れたものです。今回紹介しているIntrinsic String Manipulation Typesと組み合わせると次のようなことができます。公式ドキュメントから引用します。

type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`;
type MainID = ASCIICacheKey<"my_app">;
//   ^? "ID-MY_APP"

業務ではどう使う?

筆者がTemplate Literal TypesやIntrinsic String Manipulation Typesを業務で積極的に使っているかというと、まだ使いこなせているとは言えません。登場がTypeScript 4.1と比較的新しいからという理由もありますが、そもそも文字列リテラル型を変換してまで表現したい状況に業務内で出会えていないという理由が大きいです。

こういった機能はどちらかというとシステム開発、アプリケーション開発よりは、ライブラリ作者に向けられているような印象があります。例えばO/Rマッパーや、データベースとの接続、データベースの抽象化など、そういったシーンに向けられたライブラリなどが想像できます。

ということで、今回は本記事用にTemplate Literal Typesの素振りをしてみようと思います。

Template Literal Types における infer の挙動

Template Literal Typesにおけるinferの挙動を理解すると、この辺の処理は書くのが楽しいです。さっそくみてみましょう。

type StringToTuple1<S extends string> = S extends `${infer C1}` ? [C1] : never;
type T11 = StringToTuple1<"abcd">;
//   ^? ["abcd"]

type StringToTuple2<S extends string> = S extends `${infer C1}${infer C2}`
  ? [C1, C2]
  : never;
type T12 = StringToTuple2<"abcd">;
//   ^? ["a", "bcd"]

type StringToTuple3<S extends string> =
  S extends `${infer C1}${infer C2}${infer C3}` ? [C1, C2, C3] : never;
type T13 = StringToTuple3<"abcd">;
//   ^? ["a", "b", "cd"]

Template Literal Typesでは、${infer C1}という書き方でinferを扱うことができます。このとき、inferが2つ以上あると、先頭一文字と残りといった分かれ方をするのが特徴的です。この扱いを理解していると、先頭から再帰処理をかけて1文字ずつ変換するという型を定義することができそうです。この${$infer C1}C1はただの型パラメータ名ですので、${infer A}でも${infer CHARACTER}でもかまわないです。

camelCaseからsnake_caseに変換する型を作る

練習として、与えられた文字列をcamelCaseからsnake_caseに変換する型を考えてみましょう。変換の流れとしては、先頭から1文字ずつ確認し、大文字であれば_を付与した上で小文字に変換、小文字であればそのまま、という流れでいけそうです。1文字ずつ処理していくため再帰を採り入れましょう。

type SnakeCase<S extends string> = S extends `${infer C1}${infer C2}`
  ? C1 extends Uppercase<C1>
    ? `_${Lowercase<C1>}${SnakeCase<C2>}`
    : `${C1}${SnakeCase<C2>}`
  : S;

type T21 = SnakeCase<"itemId">; // "item_id"
type T22 = SnakeCase<"userPhoneNumber">; // "user_phone_number"
type T23 = SnakeCase<"category_tags">; // "category__tags" oops

それっぽいものを即席で書いてみました。「大文字であれば」という条件分岐はC1 extends Uppercase<C1>という記述で「自身と自身を大文字にしたものが一致すれば」という表現で再現しました。Uppercase<S>Lowercase<S>だけで十分面白いものが作れます。

このサンプルコードにはまだ満たせていない挙動があります。T23のようにSnakeCase<S>に最初からsnake_caseの文字列を与えたときの挙動が不安定なことが確認できます。これは_自体に大文字・小文字の区別がないため、大文字扱いされてしまうことが原因のようです。本稿では軽く動作を試してみることが主旨であるため一旦ここまでとしますが、もし興味があればこのSnakeCase<S>の完成度をより高めてみてください。

TypeScriptはチューリング完全

もうここまでくるとTypeScriptの型だけでプログラミングができてしまいます。実のところ、TypeScriptはチューリング完全であるためなんでもできます。

実際は再帰の制限がかかっているため、ありとあらゆるプログラムをTypeScriptの型システム上で構築するといったことは不可能ですが、世界は広く、型システム上であらゆる計算を実装してしまった人もいたりします。

https://github.com/ecyrbe/ts-calc

明日は『NonNullable<T>と実例 assertExists()

本日は、コンパイラ内部で変換処理を実行してしまうIntrinsic String Manipulation Typesと、それを活用したちょっとした小道具の作成実例を紹介しました。TypeScriptは3.x系で大半の要望を実現したと思われたのですが、まだまだ4.1でこのような機能を実装するなど進化が著しく、また面白い言語です。今後もこういった強力な機能は積極的に試していきたいものです。

明日はまた傾向を変えて、より安全なランタイムチェックについて紹介します。それではまた。

Discussion