TypeScript における型レベルバリデーション #wejs / We Are JavaScripters 36th

332f89cc697355902a817506b6995f2b?s=47 y_taka_23
September 30, 2019

TypeScript における型レベルバリデーション #wejs / We Are JavaScripters 36th

We Are JavaScripters! @36th で使用したスライドです。

幽霊型は型を詳細化する手法として有名ですが、TypeScript では構造的部分型の働きにより、ナイーブに移植しただけでは期待通り動作しません。そこで、型で値の種類を判別する手段として、Branded Type を用いた設計について解説します。

イベント概要:https://wajs.connpass.com/event/145639/

332f89cc697355902a817506b6995f2b?s=128

y_taka_23

September 30, 2019
Tweet

Transcript

  1. TypeScript における 型レベルバリデーション チェシャ猫 (@y_taka_23) We Are JavaScripters! @36th (2019/09/30)

    #wejs
  2. interface User { name: string age: number } function makeUser(name:

    string, age: number) { return { name, age } } // no error const goodUser = makeUser('John', 42) // no error... const badUser = makeUser('', -42) #wejs
  3. TypeScript の型が保証するもの • 値の「種類」は型が保証 ◦ 生の JavaScript に比べればだいぶ助かる • 値の「内容」は型では保証できない

    ◦ name: string は「空でない」文字列 ◦ age: number は「0 以上」かつ「自然数」 • ロジックのバグに対して意外と無力 ◦ できればドメイン制約も強制したい #wejs
  4. もっと型に情報を載せたい #wejs

  5. よくある手法:幽霊型 #wejs

  6. 幽霊型 (Phantom Types) • 型にパラメータを追加 ◦ Generics でパラメータ付きの型を作る ◦ ただし、その型は実際には使用しない

    • 幽霊型の使いどころ ◦ 実行時に同じデータ (ex. string) であっても、 型検査されたときに異なる型として振る舞う ◦ バリデーションの通過を型で表現できる #wejs
  7. interface StringOf<T> { value: string } const UserName = "UserName"

    type UserName = typeof UserName interface User { name: StringOf<UserName> age: number } function validateName(s: string): StringOf<UserName> { // validation logic here return { value: s } } #wejs
  8. function makeUser( name: StringOf<UserName>, age: number) { return { name,

    age } } // compilation error! const badUser = makeUser('John', 20) // no error const goodUser = makeUser(validateName('John'), 20) #wejs
  9. バリデーションを型で表現できた (age についても同様) #wejs

  10. 文字列の種別が増えたら? #wejs

  11. const UserId = "UserId" type UserId = typeof UserId Interface

    User { id: StringOf<UserId> name: StringOf<UserName> age: number } function validateId(s: string): StringOf<UserId> { // another validation logic for UserId return { value: s } } #wejs
  12. function makeUser( id: StringOf<UserId>, name: StringOf<UserName>, age: number) { return

    { id, name, age } } const myId = validateId('u0001') const myName = validateName('John') // no error const goodUser = makeUser(myId, myName, 20) // no error... const badUser = makeUser(myName, myId, 20) #wejs
  13. (幽霊型、駄目じゃん) #wejs

  14. 公称型と構造型 • 公称的部分型 (nominal subtyping) ◦ 明示的な宣言によって型を判定 ◦ 宣言がない場合は部分型だと見なさない •

    構造的部分型 (structural subtyping) ◦ 明示的に部分型関係を宣言しない ◦ 必要なフィールドを持つかどうかだけで判定 • TypeScript は構造的部分型をサポート #wejs
  15. interface StringOf<T> { value: string } const myId: StringOf<UserId> =

    validateId('u0001') const myName: StringOf<UserName> = validateName('John') // no error const goodUser = makeUser(myId, myName, 20) // no error... const badUser = makeUser(myName, myId, 20) #wejs
  16. フィールドで差を付ける必要性 #wejs

  17. Branded Types #wejs

  18. type UserId = string & { readonly _UserIdBrand: unique symbol

    } type UserName = string & { readonly _UserNameBrand: unique symbol } function validateId(s: string): UserId { // validation logic for UserId return s as UserId } function validateName(s: string): UserName { // validation logic for UserName return s as UserName } #wejs
  19. function makeUser( id: UserId, name: UserName, age: number) { return

    { id, name, age } } const myId = validateId('u0001') const myName = validateName('John') // no error const goodUser = makeUser(myId, myName, 20) // compilation error! const badUser = makeUser(myName, myId, 20) #wejs
  20. まとめ • 標準の型だとやや非力 ◦ ドメイン制約から来る「内容」が保証できない • 単純な幽霊型は期待通り動作しない ◦ 構造的部分型のせいで区別できない型が生じる •

    Branded Type を用いると便利 ◦ 複数種類の仕様がある場合でも区別できる #wejs
  21. Let’s Validate the World! Presented by チェシャ猫 (@y_taka_23) #wejs