Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TypeScript の流儀

Takepepe
September 03, 2019

TypeScript の流儀

Bonfire Frontend #4 http://yj-meetup.connpass.com/event/136480/

Takepepe

September 03, 2019
Tweet

More Decks by Takepepe

Other Decks in Technology

Transcript

  1. About Me ▪ Takefumi Yoshii / @Takepepe ▪ DeNA /

    DeSC Healthcare ▪ Frontend Engineer ▪ 実践TypeScript 著者
  2. 注釈(Annotation) function greet(): void { // 戻り値がないことを表す void型 return 'hello'

    // Error! 値を返してはいけない } const msg = greet() // const msg: void 意図的に Annotation が付与されている場合、 実装はその型に従わなければいけません。 1. 型推論いろは
  3. const / let の推論 let は再代入可能なため「string 型」に、 const は再代入不可なため「"msg"型(String Literal

    型)」に。 let msg1 = 'msg' // let msg1: string const msg2 = 'msg' // const mag2: "msg" 1. 型推論いろは
  4. Null 許容型 複数の型を表す Union Types。TypeScript では、 Nullable型(Null許容型)も、Union Types で表現します。 function

    greet(name: string | null) { let user = 'USER' if (name !== null) { user = name.toUpperCase() // (parameter) name: string } console.log(`HELLO ${user}!`) } 1. 型推論いろは
  5. Null 許容型 複数の型を パイプ | で連結し、 引数は「string型 または null型」どちらかの型であることを表します。 function

    greet(name: string | null) { let user = 'USER' if (name !== null) { user = name.toUpperCase() // (parameter) name: string } console.log(`HELLO ${user}!`) } 1. 型推論いろは
  6. ガード節 null や undefined を安全に扱う手法「ガード節」。 ガード節を通過したブロックでは、型が絞り込まれます。 function greet(name: string |

    null) { let user = 'USER' if (name !== null) { // ガード節 user = name.toUpperCase() } console.log(`HELLO ${user}!`) } 1. 型推論いろは
  7. ガード節 型が絞り込まれると、そのインスタンスに備わった プロパティ・メソッド へのアクセスが安全なものであると解釈されます。 function greet(name: string | null) {

    let user = 'USER' if (name !== null) { // ガード節 user = name.toUpperCase() // (parameter) name: string } console.log(`HELLO ${user}!`) } 1. 型推論いろは
  8. タグ付き Union Types 次の User型 は「UserA型・UserB型・UserC型」からなる Union Typesであり、共通のプロパティを保持しています。 type UserA

    = { gender: 'male'; name: string } type UserB = { gender: 'female'; age: number } type UserC = { gender: 'other'; graduate: string } type User = UserA | UserB | UserC 1. 型推論いろは
  9. タグ付き Union Types 共通プロパティ「gender」の型は「'male'・'female'・'other'」型 それぞれ異なる「String Literal 型」です。 type UserA =

    { gender: 'male'; name: string } type UserB = { gender: 'female'; age: number } type UserC = { gender: 'other'; graduate: string } type User = UserA | UserB | UserC 1. 型推論いろは
  10. タグ付き Union Types User 型のように、Literal 型で区別できる Union Types は、 「タグ付き

    Union Types」と呼びます。(別名:Discriminated Unions) type UserA = { gender: 'male'; name: string } type UserB = { gender: 'female'; age: number } type UserC = { gender: 'other'; graduate: string } type User = UserA | UserB | UserC 1. 型推論いろは
  11. タグ付き Union Types function judgeUserType(user: User) { switch (user.gender) {

    case 'male': const u0 = user // (parameter) user: UserA break case 'female': const u1 = user // (parameter) user: UserB break case 'other': const u2 = user // (parameter) user: UserC break default: const u3 = user // (parameter) user: never } } タグ付き Union Types が付 与された値を分岐にかける と、分岐ブロック内で、型が 絞り込まれます。 1. 型推論いろは
  12. タグ付き Union Types これにより、各型にしか保持 していないプロパティであっ ても、安全なアクセスである と解釈されます。 function judgeUserType(user: User)

    { switch (user.gender) { case 'male': const u0 = user.name // const u0: string break case 'female': const u1 = user.age // const u1: number break case 'other': const u2 = user.graduate // const u2: string break default: const u3 = user // const u3: never } } 1. 型推論いろは
  13. 守りの策略・Annotation インデックスシグネチャとよばれる型を付与すると、 オブジェクトプロパティの型を一律で制約することができます。 type Functions = { [k: string]: Function

    } // インデックスシグネチャ const funcs: Functions = { f1: () => true, f2: async () => false, s1: 'str' // Error! 関数として評価できない } 2. 攻防一体・型の策略
  14. 攻めの策略・Assertion 次の配列プロパティは「never」配列と推論されてしまい、 このままでは何も追加することができません。 const state = { count: 0, flag:

    false, arr: [] } const state: { count: number; flag: boolean; arr: never[]; // 望まない推論結果 } 2. 攻防一体・型の策略 推論 結果
  15. 攻めの策略・Assertion const state = { count: 0, flag: false, arr:

    [] as string[] } 実装推論では測れない部分的補足として「型解釈のヒント」を付与します。 Assertion 付与は「攻めの策略」と言い換えることができます。 const state: { count: number; flag: boolean; arr: string[]; // 望みどおりの推論結果 } 2. 攻防一体・型の策略 推論 結果
  16. User Defined Type Guard 3. コンパイラの合意 ランタイム挙動をなぞらえた型推論は、完璧ではありません。 例えば次の変数「users」から、男性のみをフィルタリングしてみます。 type Male

    = { id: string; gender: 'male' } type Female = { id: string; gender: 'female' } type User = Male | Female const users: User[] = [ { id: '1', gender: 'male' }, { id: '2', gender: 'female' }, { id: '3', gender: 'male' } ]
  17. User Defined Type Guard 3. コンパイラの合意 現在の Array.filter の推論では、型を絞り込む事はできません。 ランタイムの挙動と同じように「

    const males: Male[] 」 が望まれます。 const males = users.filter(user => { return user.gender === 'male' }) // const males: User[]; 望まない推論結果
  18. User Defined Type Guard 3. コンパイラの合意 ここに「: user is Male」という戻り型

    Annotation を付与することで、 後続の型解釈を操作することができます。 const males = users.filter((user): user is Male => { return user.gender === 'male' }) // const males: Male[]; 望み通りの推論結果
  19. User Defined Type Guard 3. コンパイラの合意 User Defined Type Guard

    を利用する場合、 プログラマが「型安全」を肩代わりしなければいけません。 const males = users.filter((user): user is Male => { return user.gender === 'female' // oops! }) // const males: Male[]; 誤った推論結果 booelan型さえ返却してれば、コンパイラは合意します。
  20. Non-null assertion インラインで型を絞りこむ「Non-null assertion」。 「 ! 」 を利用することで「null | undefined」が振るい落とされます。

    const msg = 'hello' as string | null const nullAble = msg // const nullAble: string | null const nonNullAble = msg! // const nonNullAble: string 3. コンパイラの合意
  21. Non-null assertion Non-null assertion は「コンパイラを欺く悪い慣習」という印象があります。 次のコードをみれば、この危険性もうなずけます。 const msg1 = 'str'

    as string | null const msg2 = 'str' as string | null const msg3 = null as string | null msg1.toUpperCase() // コンパイルエラーになるが、ランタイムエラーにならない msg2!.toUpperCase() // コンパイルエラーにならず、ランタイムエラーにならない msg3!.toUpperCase() // コンパイルエラーにならず、ランタイムエラーになる 3. コンパイラの合意
  22. Non-null assertion しかしながら、Non-null assertion は悪い慣習とは限りません。 特定のケースにおいて、有効なことがあります。 const msg1 = 'str'

    as string | null const msg2 = 'str' as string | null const msg3 = null as string | null msg1.toUpperCase() // コンパイルエラーになるが、ランタイムエラーにならない msg2!.toUpperCase() // コンパイルエラーにならず、ランタイムエラーにならない msg3!.toUpperCase() // コンパイルエラーにならず、ランタイムエラーになる 3. コンパイラの合意
  23. Non-null assertion // getElementById(elementId: string): HTMLElement | null; document.getElementById('btn')!.addEventListener('click', ()

    => {}) 3. コンパイラの合意 「プログラマが品質担保します」という署名を信じ、コンパイラは合意します。 「Non-null assertion」はコンパイラを欺くためのものではなく、 「品質担保します」という意思表示に他なりません。
  24. const assertion この署名を行った場合、JavaScript 本来の挙動と異なる「厳格さ」が 与えられてしまうことに注意しなければいけません。 let user2 = 'taro' //

    let user2: string let user3 = 'taro' as const // let user3: "taro" user2 = 'TARO' user3 = 'TARO' // Error; JavaScript とは異なる挙動 3. コンパイラの合意
  25. const assertion この署名を行った場合、JavaScript 本来の挙動と異なる「厳格さ」が 与えられてしまうことに注意しなければいけません。 let user2 = 'taro' //

    let user2: string let user3 = 'taro' as const // let user3: "taro" user2 = 'TARO' user3 = 'TARO' // Error; JavaScript とは異なる挙動 3. コンパイラの合意 「JSの挙動と異なってもよい」という署名を信じ、コンパイラは合意します。
  26. 「頑張る = 厳格」とは限らない 4. 型の主従関係 次の関数定義において与えらた型情報は、 引数の Annotation「: number」のみです。 import

    { SET_COUNT } from './actionTypes' export function setCount(amount: number) { return { type: SET_COUNT, payoad: { amount } } }
  27. 「頑張る = 厳格」とは限らない 4. 型の主従関係 それでいて、戻り型まで厳格な型(String Literal 型)が得られています。 "LONG_PREFIX_SET_COUNT"型 は、この定義内のどこにもありません。

    import { SET_COUNT } from './actionTypes' export function setCount(amount: number) { return { type: SET_COUNT, payoad: { amount } } } // function setCount(amount: number): { // type: "LONG_PREFIX_SET_COUNT"; payoad: { amount: number; }; // }
  28. 「頑張る = 厳格」とは限らない 4. 型の主従関係 この String Literal 型は、上流工程で既に定められていたものです。 次の様に

    const assertion が付与されていました。 export = { INCREMENT: 'LONG_PREFIX_INCREMENT', DECREMENT: 'LONG_PREFIX_DECREMENT', SET_COUNT: 'LONG_PREFIX_SET_COUNT' } as const
  29. 「上流下流 = 依存関係 = 型の主/従」 4. 型の主従関係 「上流工程なのか・下流工程なのか」は一目瞭然で、 ファイル上部の import

    を見ればすぐにわかります。 export function isNumberLikeString(value: string) { return !value.match(/[^-^0-9^.]/g) } // function isNumberLikeString(value: string): boolean
  30. 「上流下流 = 依存関係 = 型の主/従」 4. 型の主従関係 何も import していなければ、そこは最上流ということができます。

    型定義だけでなく「実装そのもの」が最上流になり得ます。 export function isNumberLikeString(value: string) { return !value.match(/[^-^0-9^.]/g) } // function isNumberLikeString(value: string): boolean 「型定義 > 実装」ではなく「上流 > 下流」である
  31. 中流構築が捗る Utility Types 5. 源流を辿る型定義 この typeof キーワードで導出した型を加工してみます。 「Partial」は全てのプロパティを「Optional」に変換する Unility

    Types です。 type Injects = { user_id?: string | undefined; name?: string | undefined; tasks?: Task[] | undefined; } type UserState = typeof userState type Injects = Partial<UserState>
  32. 中流構築が捗る Utility Types 5. 源流を辿る型定義 TypeScript にあらかじめビルトインされた「Utility Types」を利用すると、 既出の型から新しい型定義を創出することができます。 type

    Injects = { user_id?: string | undefined; name?: string | undefined; tasks?: Task[] | undefined; } type UserState = typeof userState type Injects = Partial<UserState>
  33. 中流構築が捗る Utility Types 5. 源流を辿る型定義 推論で得られる型は次のとおりです。 type StoreState = {

    user: { user_id: string; name: string; tasks: Task[]; }; app: { initalized: boolean; isConnecting: boolean; }; }
  34. Conditional Types が強力なわけ 5. 源流を辿る型定義 「Conditional Types」は 型の三項演算子です。 Generics に与えた型「T」が、比較対象型「number」と

    互換性がある場合、任意型を導きます。 type IsNumber<T> = T extends number ? true : false type T1 = IsNumber<1> // type T1 = true type T2 = IsNumber<'2'> // type T2 = false
  35. Conditional Types が強力なわけ 5. 源流を辿る型定義 Conditional Types では、比較対象型の「部分導出」が可能です。 組み込み Utility

    Types の「ReturnType」も、これを利用しています。 type ReturnType<T> = T extends (...args: any) => infer I ? I : any
  36. TypeScript の流儀「十訓」 ▪ 型情報がなくても、実装に型はついて回る ▪ 型推論は JavaScript の構文をなぞらえる ▪ 型は束縛されるものではなく、策略を練るもの

    ▪ 策略の通達は、必要に応じて随時行う ▪ 攻めの策略には、隙が生まれることを心得る ▪ 合意に基づき、プログラマが品質を担保する ▪ 型定義が上流工程とは限らない ▪ 依存関係が型の主従関係そのものである ▪ 下流工程はそのまま受け流すことが最も厳格 ▪ 複雑な型定義は、源流を辿るためにある