目次
- 背景
- 既存の問題
- Branded Type とは
- ユースケース
- 実践
- 最後に
1. 背景
先日あったSansan様のセミナーでBrandedTypeについて初めて知った為です。
こちらがプレゼンに使われていた資料です。
元ネタはこちらです。
次節から元ネタの方を和訳して説明します。
2. 既存の問題
以下をご覧ください。
function print(age:number,height:number) {
console.log('ageを表示:',age);
console.log('heightを表示:',height);
}
const age = 25;
const height = 175;
print(height,age);
引数の順番を間違えてしまいましたが、どちらも同じstring型を受けっているのでTypescriptではエラーにはなりません。
しかし、意図した処理にはなってないです。
実行後にこの呼び出しに問題があることが判明してしまいます。
3. Branded Type とは
既存の型に属性やラベルを追加した型です。
例
type BrandType<K, T> = K & { __brand: T }
type AgeType = BrandType<number, "age">
type HeightType = BrandType<number, "height">
これはAgeTypeという新しい型を作成し、_brand:"age"というkey,valueのみを持つオブジェクトと関連付けます。
このBrandTypeで宣言された型は、number型であることに加えて_brandプロパティの値と一致することを強制します。
BrandTypeを使って、前の例を書き直してみます。
type BrandType<K, T> = K & { __brand: T }
type AgeType = BrandType<number, "age">
type HeightType = BrandType<number, "height">
function print(age:AgeType,height:HeightType) {
console.log('ageを表示:',age);
console.log('heightを表示:',height);
}
const age = 25 as AgeType;
const height = 175 as HeightType;
print(height,age);
/**Argument of type 'number' is not assignable to parameter of type 'AgeType'.
Type 'number' is not assignable to type '{ __brand: "age"; }'. */
しかし、まだこれだけだと問題があります。
型を "タグ付け"するために使われる__brandプロパティは、"ビルド時"のみのプロパティです。
このプロパティを参照する処理を書くと、実行時にこのプロパティが存在しないため、undefinedになって問題が発生する可能性があります。
type BrandType<K, T> = K & { __brand: T }
type AgeType = BrandType<number, "age">
type HeightType = BrandType<number, "height">
function print(age:AgeType,height:HeightType) {
console.log('ageのbrandedプロパティを表示:',age.__brand);
console.log('heightのbrandedプロパティを表示:',height.__brand);
}
const age = 25 as AgeType;
const height = 175 as HeightType;
print(age,height);
実行結果
"ageのbrandedプロパティを表示:", undefined
"heightのbrandedプロパティを表示:", undefined
次に改良されたBrandedTypeを紹介します。
declare const __brand: unique symbol
type Brand<B> = { [__brand]: B }
export type Branded<T, B> = T & Brand<B>
unique symbolについてはこちらをご覧ください
declareについてはこちらをご覧ください
- 「__brand」という名前のハードコードされたプロパティを使う代わりに、計算されたプロパティを使うことができます。
- 前述のプロパティの重複を避けるために、ユニークなシンボルを使用することができます。
- プロパティへの読み取りアクセスを禁止します。
declare const __brand: unique symbol
type BrandType<B> = { [__brand]: B }
export type BrandedType<T, B> = T & BrandType<B>
type AgeType = BrandedType<number, "age">
type HeightType = BrandedType<number, "height">
function print(age:AgeType,height:HeightType) {
console.log('ageを表示:',age);
console.log('heightを表示:',height);
}
const age = 25 as AgeType;
const height = 175 as HeightType;
print(height,age);
/**Argument of type 'HeightType' is not assignable to parameter of type 'AgeType'.
Type 'HeightType' is not assignable to type 'BrandType<"age">'.
Types of property '[__brand]' are incompatible.
Type '"height"' is not assignable to type '"age"'. */
playground
上のように[__brand]プロパティの型に互換性がない為、エラーになります。
ここで一つ個人的に引っかかります。
ブランドプロパティにアクセスできてしまうという課題をこのplaygroundですと確認できません。
上のコードを各ファイルに分けたものが以下です。
cloneしてコメントアウトしてエラーになるか確認して見てください。
コード
確かに[__brand]プロパティにアクセスできないことを確認できました。
4. ユースケース
カスタムバリデーション
type EmailAddress = Brand<string, "EmailAddress">
function validEmail(email: string): EmailAddress {
// email address validation logic here
return email as EmailAddres;
}
メールアドレスが適切なフォーマットである場合にEmailAddress型で返します。
ドメインモデル
type CarBrand = Brand<string, "CarBrand">
type EngineType = Brand<string, "EngineType">
type CarModel = Brand<string, "CarModel">
type CarColor = Brand<string, "CarColor">
自動車の製造ラインでは、異なる機能や種類の自動車があります。
function createCar(carBrand: CarBrand, carModel: CarModel, engineType: EngineType, color: CarColor): Car {
// ...
}
const car = createCar("Toyota", "Corolla", "Diesel", "Red") // Error:
// "Diesel" is not of type "EngineType"
APIのレスポンスとリクエスト
type ApiSuccess<T> = T & { __apiSuccessBrand: true }
type ApiFailure = {
code: number;
message: string;
error: Error;
} & { __apiFailureBrand: true };
type ApiResponse<T> = ApiSuccess<T> | ApiFailure;
APIコールの成功と失敗を区別するために、特定のAPIでブランドを使用しています。
const response: ApiResponse<string> = await fetchSomeEndpoint();
if (isApiSuccess(response)) {
// handle success response
}
if (isApiFailure(response)) {
// log error message
}
5. 実践
以下を作成してください
- 人の年齢のための新しいブランド型
- 数値入力を受けてそれをAgeとして返す関数createAge
- およびAgeを受け取って数値を返す関数getBirthYear
- 0<=Age<=125
/** You can work on this challenge directly in the typescript playground: https://tsplay.dev/WzxBRN*/
/** For this challenge you need to create the Branded utility type */
type Age = Branded<never, "Age">; // Replace the never with the corresponding primitive type
/**
* 🏋️♀️ Fill the function createAge, it should accept a number as input and return a branded Age
**/
function createAge() {
// Perform logic and return Age
}
/**
* 🏋️♀️ This function should accept a branded Age type and return a number
*/
function getBirthYear() {
// Perform logic and return Age
}
/** Usage **/
const myAge = createAge(36); // Should be ok
const birthYear = getBirthYear(myAge) // Should be ok
const birthYear2 = getBirthYear(36) // This should show an error
/** Type Tests */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<typeof myAge, Age>>,
Expect<Equal<typeof birthYear, number>>,
Expect<Equal<Parameters<typeof createAge>, [number] >>,
Expect<Equal<Parameters<typeof getBirthYear>, [Age] >>
]
6. 最後に
BrandedTypeはprimitive型の型安全性を高めるのに役立つと思いました。
7. 参考
参考になる記事を書いてくださった皆様ありがとうございました!
宣伝させてください!