👻

【TS】`${number}` 型に入るアルファベット、いくつ言える?【テンプレートリテラル型の重箱の隅】

2024/02/24に公開3

TypeScript の テンプレートリテラル型 (Template Literal Types) をご存知でしょうか。
string型の部分型を作り出すことができ、より厳密な文字列の型指定ができます。

例えば `${number}` という型を使うと、テンプレートリテラルで数値を出力したときに現れそうな文字列("123" など)のみを受け入れる型を作ることができます。

// ✅数字ならOK
const ok1: `${number}` = `${123}`; // "123"
const ok2: `${number}` = "123";
const ok3: `${number}` = `${0}`; // "0"
const ok4: `${number}` = "20240224";

// ❌これらはエラー
const error1: `${number}` = "abc";
const error2: `${number}` = "2024年2月24日";

さて、この ${number} 型には数字(0-9)しか入らないでしょうか?
この記事では(たぶん実用上気にする必要はないところまで)`${number}` に入りうる文字列を調べてみます。

確認したバージョン

TypeScript v5.3.3

`${number}` に入りうる文字

数字 0-9

まずは基本から確認していきます。数字だけで構成された文字列はもちろん OK です。

const numbers: `${number}`[] = [
  // ✅これらはすべてOK
  "0",
  "1",
  "2",
  "3776",
  "20240224",
];

テンプレートリテラルを使って数値を文字列化して同じ結果が得られるので、当然ですね。

const numbers = [
  `${0}`, // "0"
  `${1}`, // "1"
  `${2}`, // "2"
  `${3776}`, // "3776"
  `${20240224}`, // "20240224"
];

符号・小数点 + - .

数値は自然数だけではありません。負の数もありますし、小数もあります。
そのため、プラス・マイナスの符号、小数点がついた文字列も入ります。

// ✅符号付きや小数の形式もOK
const signed: `${number}`[] = ["-1", "+1"];
const float: `${number}`[] = ["3.14", "0.1", ".1", "5."];

少し面白いのは、 テンプレートリテラルを使っても現れない文字列も受け入れている ということです。
`${+1}`"1" になりますが、 "+1" という文字列は `${number}` 型を満たしますし、
`${.1}`"0.1" になりますが、 ".1" という文字列も `${number}` 型を満たします。

`${+1}`; // "1"("+1" にはならない)
`${.1}`; // "0.1"(".1" にはならない)
`${5.}`; // "5"("5." にはならない)

指数表記 e

JavaScript では、数値を指数表記で書くことができます。
<m>e<n> という形式で、 m\times10^n の数値を表します。
この e は大文字Eでも OK です。

JavaScriptで指数表記を使う例
const kilo = 1e3; // 1000
const micro = 1.E-6; // 0.000001

桁数が多い数値を扱おうとした場合は、自動的に変換されます。文字列化した場合にも指数表記になります。

JavaScriptで浮動小数に丸められる例
const large = 123456789012345678901234567890;
console.log(large); // 1.2345678901234568e+29
console.log(`${large}`); // "1.2345678901234568e+29"

そのため、 `${number}` 型には、この指数表記の文字列も入ります。

// ✅指数形式もOK
const exponential: `${number}`[] = [
  "1e3",
  "6.02e+23",
  "1.E-6",
  "1.2345678901234568e+29",
];

n 進数表記 b o x、16 進数表記 a b c d e f

JavaScript では、2 進数、8 進数、16 進数の表記ができます。

JavaScriptで進数表記を使う例
const binary = 0b1010; // 10
const octal = 0o123; // 83
const hex = 0xff; // 255

`${number}` 型はこれらの表記を文字列化した値を含むため、b o xも含まれます。
そして、16 進数が含まれるということは、a b c d e f も含まれるということです。
(これらの大文字も含まれます)

// ✅n進数表記もOK
const binary: `${number}`[] = ["0b1010", "0B1010"];
const octal: `${number}`[] = ["0o123", "0O123"];
const hex: `${number}`[] = ["0xabcdef", "0XABCDEF"];

ここでまた不思議なのは、 これらの n 進数表記はテンプレートリテラルを使っても現れない文字列である 点です。
n 進数表記で書いたとしても、値としては 10 進数として扱われるため、テンプレートリテラルで出力しても 10 進数の文字列になります。
なので `${number}` 型が "0b1010""0o123" を受け入れるのはよくわかりませんでした 🤔

`${0b1010}`; // "10"("0b1010" にはならない)

ですが、 8 進数表記の文字列を受け入れていることで、0 から始まる数値が受け入れられている 、というのはあるのかもしれません。
実は JavaScript では、0 から始まる数値はエラーになりません [1]。 0 から始まり 0-7 だけで構成された数値は 8 進数として扱われ、8,9 が含まれると 10 進数として扱われます。

JavaScriptで0から始まる数値を使う例
const octal = 0123; // 8進数 の 123 として扱われる
console.log(octal); // 83
const decimal = 00789; // 10進数 の 789 として扱われる
console.log(decimal); // 789

(そういう背景なのかは不明ですが、)`${number}` 型は 0 から始まる数字列を受け入れます。

const numbers: `${number}`[] = ["0123", "00789"];

Infinity, NaN (テンプレートリテラルの場合)

JavaScript には、無限大を表す Infinity と非数を表す NaN という特殊な値があります。
Infinity NaN の型は number です。
そのため、 Infinity NaN をテンプレートリテラルで出力するようなコードは型エラーになりません。

// ✅Infinity, NaN も型エラーにならない
const values: `${number}`[] = [
  `${Infinity}`, // "Infinity"
  `${1 / 0}`, // "Infinity"
  `${NaN}`, // "NaN"
  `${Number("数字じゃないよ")}`, // "NaN"
];

ただし、文字列リテラルとして入れることはできません(これまでの例では「文字列リテラルでは書けるが、テンプレートリテラルでは現れない」ということが多かったのですが、ここでは逆ですね)。

// ❌これらはエラー
const errors: `${number}`[] = ["Infinity", "NaN"];

`${number}` に入らない文字

これまでの例があるなら入るかと思ったけれど入らない例もいくつかありました。

_ (アンダースコア)は入らない

JavaScript では、数値を区切るために _(アンダースコア)を使うことができます。
これは桁数の多い数値を読みやすくするための記法で、値には影響しません。

const million = 1_000_000; // 1000000

しかし、 `${number}` には _ を含めることはできません。

// ❌これはエラー
const error: `${number}` = "1_000";

n (bigint)は入らない

JavaScript では、bigint という型があり、 number では表すことのできない大きな整数を表すことができます。
末尾にnを加えることで、bigint型のリテラルを作ることができます。

JavaScriptでbigintを使う例
const big = 12345678901234567890n;
typeof big; // "bigint"

`${number}` には n を含めることはできません。
まあそもそもnumberbigintは別の型なので入らないのは当たり前なのですが、 `${bigint}` で試してもエラーになります。

// ❌これはエラー
const error1: `${number}` = "12345678901234567890n";
// ❌これもエラー
const error2: `${bigint}` = "12345678901234567890n";

これは bigint をテンプレートリテラルで文字列化(つまり toString())したとしても n が含まれないためだと思われます。

`${12345678901234567890n}`; // "12345678901234567890"(`n` は含まれない)

まとめ

`${number}` 型に入りうる文字は、数字だけではない。入りうる文字は以下の通り。

  • 数字: 0 1 2 3 4 5 6 7 8 9
  • 符号: + -
  • 小数点: .
  • 指数表記: e および E
  • 進数表記: b o x および B O X
  • 16 進表記: a b c d e f および A B C D E F
  • Infinity(テンプレートリテラルの場合)
  • NaN(テンプレートリテラルの場合)

(これ以外にも考慮漏れがあればコメントで教えてください!)

つまり、数字だけを入れたいと思って `${number}` 型を使っても、数字以外の文字列が入る可能性があるということです。
ただ、そこまでの厳密さを求めるケースはあまりない(少なくとも私にとっては)ので、実用上は `${number}` 型で問題ないと思います。

どうしても数字だけを受け入れたい場合

0-9 だけで構成される文字列を受け入れる型を作りたい場合は、以下のように定義してしまうとよいでしょう。
(どうにかすれば任意の桁数に対応できたりするのかもしれませんが、自分には書けそうにありませんでした)

type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
/** 3桁までの数字だけで構成される文字列 */
type DigitString = `${Digit}` | `${Digit}${Digit}` | `${Digit}${Digit}${Digit}`;

任意の文字列長に対して 0-9 だけで構成される文字列かどうかをチェックしたければ、正規表現を使って実行時に判定するのが確実かつわかりやすそうです。

/** 数字だけで構成される文字列かどうかをチェックする関数 */
const isDigitString = (str: string): boolean => /^\d+$/.test(str);
脚注
  1. 0 から始まる数値は、TypeScript ではエラーになります。0123のように 8 進数として扱われる値を使おうとすると0o123を使うようにエラーが出ますし、0789のように 10 進数として扱われる値を使おうとすると先頭の 0 を取り除くようにエラーが出ます。 ↩︎

GitHubで編集を提案

Discussion

五所 和哉 (MonCargo CTO)五所 和哉 (MonCargo CTO)

記事ありがとうございます。

0-9 だけで構成される文字列を受け入れる型を作りたい場合は、以下のように定義してしまうとよいでしょう。
(どうにかすれば任意の桁数に対応できたりするのかもしれませんが、自分には書けそうにありませんでした)

面白そうなのでトライしてみました。
再帰的に型判定を実行することで、それっぽいものを定義できました!

type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

type CheckDigitString<T extends string> = T extends Digit
  ? true
  : T extends `${Digit}${infer Next}`
  ? CheckDigitString<Next>
  : false

かがんかがん

コメントありがとうございます!!
なるほど、型引数で渡すのであればいけますね…!

ただそれだと const sample: CheckDigitString = "123"; みたいな書き方はできないということですよね… 🤔
どうにか値をチェックするような書き方を考えていたのですが、以下のように別の行でチェックすればできそうでした

const sample1 = "123";
true satisfies CheckDigitString<typeof sample1>; // OK

const sample2 = "abc";
true satisfies CheckDigitString<typeof sample2>; // 型エラーになる

配列の場合だともう少し複雑になって、こう。

//  `true | false` の可能性があるので `satisfies` だけでは判定できず、厳密に `true` かどうかをチェックする
type CheckTrue<T extends true> = T;

const sample3 = ["123", "456"] as const;
true satisfies CheckTrue<CheckDigitString<typeof sample3[number]>>; // OK

const sample4 = ["123", "abc"] as const;
true satisfies CheckTrue<CheckDigitString<typeof sample4[number]>>; // 型エラーになる

TS Playgroundでのサンプル

なかなか複雑になってしまいますが、型としてチェックできますね! 👏