はじめに
皆さん、こんにちは。新卒2年目の細川です。普段は、TypeScriptを使って開発をしてます。
突然ですが皆さん、TypeScriptでもっと厳密に型付けをしたいなと思ったことはありませんか?
以前こちらのブログでも触れたとおり、TypeScriptでは構造的部分型という緩めの型付けを採用しています(参考)。
構造的部分型は便利な面もありますが、厳密に型付けをしたい場合には少し頼りない部分もあります。そこで、TypeScriptで、より厳密に型付けをする方法を紹介していきたいと思います。
もし、間違ってるところありましたら優しく教えていただけると助かります。
構造的部分型の復習
構造的部分型について少し復習してみましょう。
例えば以下の2つの型を比べてみましょう。
type User = {
id: string;
name: string;
};
type Company = {
id: string;
name: string;
};
どちらもidとnameというプロパティを持っています。
この2つは違う型として定義していますが、TypeScriptはこの2つの型をきちんと区別していません。
const user: User = { id: "uid", name: "hoge" };
const company: Company = user; // Company型にUser型を代入できる
このように、構造的部分型は型の構造(シグネチャ)に重きを置いており、その型に必要なプロパティを持っているかどうかしかチェックしていません。イメージとしては、idとnameを持つ型にUserというエイリアスをつけよう!みたいなイメージです。
せっかくUser型とCompany型を定義したのに、同じ型として扱われたら、困りますよね?
それぞれをしっかり別の型として扱ってほしい!みたいなこともあると思います。
より厳密に型付けを行う方法
同じプロパティを持っている型でも違う型として扱うためには、Branded Typesというものを使います。
まずは先ほどのUser型とCompany型を以下のように書き換えます。
type User = {
id: string;
name: string;
} & {_brand: "user"};
type Company = {
id: string;
name: string;
} & {_brand: "company"};
そうするとuserを定義する行で、元のコードだとエラーが出るようになるので、as Userを追加します。
const user: User = { id: "uid", name: "hoge" } as User; // as Userを追加
const company: Company = user; // 型 'User' を型 'Company' に割り当てることはできません。
こうすることで、Company型の変数にUser型の変数は代入できなくなります。
これは&を使ったTypeScriptのIntersection Typesの応用です。
_brand
というプロパティを持ったオブジェクトとのインターセクション型にすることで、それ以外のプロパティが、同じでも型を区別することができます。
省略の仕方
これで、同じプロパティを持つ型でも区別ができるようになりましたが、毎回& {_brand: “user”}
みたいなものをつけるのって面倒くさいですよね。そこで、以下のような定義を最初に記述することで、省略することができます。
定義しておけば、新しく型を定義するときに、Brand<>
で囲ってやることで、他の型と区別ができるようになります!
type Brand<K, T> = K & { __brand: T }
// 使い方
type User = Brand<{id: string; name: string}, "user">
type Company = Brand<{id: string; name: string}, "company">
// 変数の宣言(上と同様)
const user: User = { id: "uid", name: "hoge" } as User;
const company: Company = { id: "cid", name: "hoge" } as Company;
変数定義の際にasを使いたくない!
これで、どんな型でも、Brandで囲うことで、他の型と区別することができるようになりました!ただし、Branded TypesであるUser型の値を宣言するときにasが必要になります。
asを使いたくない場面もありますよね。
そんな時は以下のようにuserを作ってあげる関数を作りましょう
type createUser = (id: string, name: string) => User;
const createUser: createUser = (id, name) => {
const user: User = {id: id, name: name, __brand: "user"}
return user
};
const user = createUser("uid", "hoge")
こうすることで、asを使わなくても、新しい値を作ることができます。ちなみに、このように型と値(今回は関数)に同じ名前を付けて同様に使うテクニックをコンパニオンオブジェクトと呼ぶみたいです。
まとめ
TypeScriptで厳密な型付けをしたい場合は、Branded Typesを使う
type Brand<K, T> = K & { __brand: T }
// 使い方
type User = Brand<{id: string; name: string}, "user">
Branded Types型の値の宣言をするときはcreateUserのような関数を使うとよい
type createUser = (id: string, name: string) => User;
const createUser: createUser = (id, name) => {
const user: User = {id: id, name: name, __brand: "user"}
return user
};
const user = createUser("uid", "hoge")
おわりに
今回は、Branded Typesを使ってTypeScriptでより厳密に型付けをする方法について紹介してみました!
TypeScriptはフロントもバックも一つの言語で書けるという面もありますが、型安全な言語に比べると、型付けがゆるく、心許ない面もあると思います。少しでも型安全に書くための仕組みがたくさんありますので、今後も勉強していきたいと思います。
以前の投稿はこちら