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

TypeScript の流儀

Avatar for Takepepe Takepepe
September 03, 2019

TypeScript の流儀

Avatar for Takepepe

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 の構文をなぞらえる ▪ 型は束縛されるものではなく、策略を練るもの

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