TypeScriptは基本的には構造的型(Structural type system)なので
type Human = {
name: string
age: number
}
と
type Dog = {
name: string
age: number
}
を型として区別する事が少なくとも現時点ではできません。構造が同じであれば同じ型と見做すからです。なので例えばstring型と同じ構造をもった別の型としてHiragana型を扱おうとして
type Hiragana = string
const charIsHiragana = (char: string) char is Hiragana => {
if (char.length > 1) {
return false
}
const codePoint = char.codePointAt(0)
return (codePoint >= 12353 && codePoint <= 12435)
}
const hiraganaToKatakana = (hiragana: Hiragana): Katakana => {/* 中略 */}
const charToHiragana = (char: string) => {
if (charIsHiragana(char)) {
// charがHiragana型の時は許される
return hiraganaToKatakana(char)
} else {
// charがHiragana型じゃない時は型検査で許されないで欲しいが、string型であれば構造が一致するので許されてしまう
return hiraganaToKatakana(char)
}
}
みたいに書いてもそもそもType AliasにしかならずHiraganaとstringを区別できないため型検査上意図した動きにはなりません。こういう時の解決方法の一つは型上の構造(実体ではなく)を変え、アサーション(やTypeGuard)を利用する事で別々の型として扱ってもらうという方法です。
// 他に存在しないプロパティ名を型上に生やす
type Hiragana = string & { _hiraganaBrand: never }
type Katakana = string & { _katakanaBrand: never }
// 共通した名前のプロパティを持っていてもそのプロパティの型が異なれば別々の型として扱われる
// 文字列リテラル型でもok
type Hiragana2 = string & { _brand: 'Hiragana' }
type Katakana2 = string & { _brand: 'Katakana' }
運用でカバー的な泥臭さは感じますがこのbrandingを利用すれば、本来の意図通りひらがな(charIsHiraganaを通過したstring)以外が誤ってhiraganaToKatakanaへ渡される事を防げます。また広く使うなら
type Brand<K, T> = K & { __brand: T }
type Hiragana = Brand<string, 'Hiragana'>
みたいなBrand型を用意してしまうのも良いんじゃないでしょうか。