5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Branded Type について理解する

Last updated at Posted at 2023-09-17

目次

  1. 背景
  2. 既存の問題
  3. Branded Type とは
  4. ユースケース
  5. 実践
  6. 最後に

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"; }'. */

playground

しかし、まだこれだけだと問題があります。
型を "タグ付け"するために使われる__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

playground

次に改良された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してコメントアウトしてエラーになるか確認して見てください。
コード
image.png
確かに[__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] >>
]

動作確認用のplyaground

回答です

6. 最後に

BrandedTypeはprimitive型の型安全性を高めるのに役立つと思いました。

7. 参考

参考になる記事を書いてくださった皆様ありがとうございました!

宣伝させてください!

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?