型で正しい状態の場合分けを列挙して表現する
以下の2つは同じ意味ではないよねという話
code:1.ts
type X = {
a: A1 | A2;
b: B1 | B2
}
code:2.ts
type X = { a: A1; b: B1 }
| { a: A2; b: B2 }
仕様として以下のようなものをが許容されないのであれば、2.tsのように定義すべき
code:ts
type Y = {
a: A1
b: B2
}
A1,A2,B1,B2の取りうる値が1つだったとしても、前者は4パターン取りうる
テストのパターンも倍になる
型は仕様なので、それで表現した値は全部許容される、と明言したことになる 仕様にふさわしいのが2.tsなのであればそう書くべき
もちろん1.tsの方がふさわしい仕様の場合もあるmrsekut.icon
例えば、以下のような仕様があるとする
Userは、必ずEmailか電話番号の情報を持っている
この仕様を満たすように型を定義するとどうなるか?
例えば、こう書ける
code:ts
type User = {
name: UserName;
email: Email;
phone: PhoneNumber;
}
しかし、これだと両方必須になってしまうので、
例えばこう修正する
code:ts
type User = {
name: UserName;
email: Email | undefined;
phone: PhoneNumber | undefined;
}
しかし、これも間違い。
この型の場合、emailとphoneの両方がundefinedなものも許容してしまう
code:ts
const user1: User = {
name: 'mrsekut',
email: undefined,
phone: undefined
}
型が仕様を正しく表現していない
例えば、こうする
code:ts
type User = {
name: UserName;
email: Email;
phone: undefined;
} | {
name: UserName;
email: undefined;
phone: PhoneNumber
} | {
name: UserName;
email: Email;
phone: PhoneNumber;
}
場合分けは、user.email != nullのように、「undefinedじゃない」という条件式でしか絞り込めない
あるいはこうする
code:ts
type User = {
type: 'emailOnley';
name: UserName;
email: Email;
} | {
type: 'phoneOnly';
name: UserName;
phone: PhoneNumber
} | {
type: 'both';
name: UserName;
email: Email;
phone: PhoneNumber;
}
あるいはこう
userを丸ごとtagged unionにする必要がないので、Contact部分のみをそうする
code:ts
type User = {
name: UserName;
contact: Contact;
}
type Contact = {
type: 'emailOnly';
email: Email
} | {
type: 'phoneOnly';
phone: PhoneNumber;
} | {
type: 'both';
email: Email;
phone: PhoneNumber;
}
Haskellならこうするかな
code:hs
data User = User
{ name :: UserName
, contact :: Contact
}
data Contact = EmailOnly Email
| PhoneOnly Phone
| Both BothContactMethods
data BothContactMethods = BothContactMethods Email Phone
data Email = Email String
data Phone = Phone String
参考
正しい状態(自体)をunionする
code:hs
data ContactInfo
= EmailOnly Email
| PostalOnly Postal
| Both Email Postal
仕様的に空になることがないなら、それも型で表現する
code:hs
type Order = { items :: NonEmptyArray Item }