Programming TypeScript ch6 (3/3) Advanced Function Types

TypeScript勉強メモ

出典: 


Advanced Function Types

Improving Type Inference for Tuples

  • TSのtupleの推論はゆるゆる
let a = [1, true] //  (number|boolean)[]
  • こうすると厳しく推論される
function tuple<
  T extends unknown[]
  >(
    ...ts: T
  ): T {
    return ts
  }


let a = [1, true] //(number|boolean)[]
let b = tuple(1, true) // [number, boolean]

User-Defined Type Guards

function isString(a: unknown): boolean {
  return typeof a === 'string'
}

const a = isString('a') // boolean
const b = isString([7]) // boolean


function parseInput(input: string | number) {
  let formattedInput: string
  if(isString(input)) {
    // here input is (string|number)
    formattedInput = input.toUpperCase() // Error: Property 'toUpperCase' does not exist on type 'string | number'.
  }
}
  • 静的な情報がないためinputをrefineできない
  • isStringのreturn typeを a is stringにすることで改善可能
function isString(a: unknown): a is string {
  return typeof a === 'string'
}

const a = isString('a') // boolean
const b = isString([7]) // boolean


function parseInput(input: string | number) {
  let formattedInput: string
  if(isString(input)) {
    // here input is string
    formattedInput = input.toUpperCase()
    return
  }
  // here input is number
  input
}

Conditional Types

type IsString<T> = T extends string ? true : false

type A = IsString<string> // true
type B = IsString<number> // false

Distributive Conditionals

  • 分配法則
  • これは、それはそう
type ToArray<T> = T[]
type A = ToArray<number> // number[]
type B = ToArray<number|string> // (number|string)[]
  • conditional typesを使うと
type ToArray2<T> = T extends unknown ? T[] : T[]
type C = ToArray2<number> // number[]
type D = ToArray2<number|string> // number[]|string[]
  • union typesがconditionalのbranchにバラされる
  • union typesから所定の型を取り除くWithout<T,U>なんかを作れる:
type Without<T,U> = T extends U ? never : T

type A = Without<
  boolean | number | string,
  boolean
> // string|number
  • 導出
type Without<T,U> = T extends U ? never : T

type A = Without<
  boolean | number | string,
  boolean
> // string|number

type A2 = Without<boolean, boolean>
  | Without<number, boolean>
  | Without<string, boolean>

type A3 = (boolean extends boolean ? never : boolean)
  | (number extends boolean ? never : number)
  | (string extends boolean ? never : string)

The infer Keyword

type ElementType<T> = T extends unknown[] ? T[number] : T
type A = ElementType<number[]> // number
type B = ElementType<boolean> // boolean


type ElementType2<T> = T extends (infer U) ? U : T
type C = ElementType<number[]> // number
type D = ElementType<boolean> // boolean
  • コンテキストからUが推論される
  • 関数の引数の情報を静的に取得したりできる
type SecondArg<F> = F extends (a: any, b:infer B) => any ? B : never

type F = typeof Array['prototype']['slice'] //  (start?: number|undefined, end?: number|undefined) => any[]
type A = SecondArg<F> // (number|undefined)

Build-in Conditional Types

  • Exclude<T, U>

    • さっき作ったWithoutとおなじ
type A = number | string | boolean
type B = boolean | typeof Array
type C = Exclude<A, B> // string | number
  • Extract<T, U>

    • TのうちUに代入可能なものを抽出
type A = number | string | boolean
type B = boolean | typeof Array
type C = Extract<A, B> // boolean
  • NonNullable<T>
type A = {a?: number | null}
type B = NonNullable<A['a']> // number
  • 【補】既存の型のnullableを外した型を得る
type A = {
  a: string|boolean|null
  b: number[]
}

type B = {
  [K in keyof A]: NonNullable<A[K]>
}
// {
//  a: string|boolean|null
//  b: number[]
// }
  • ReturnType<F>

    • 関数Fの戻り値
    • 【補】自分で作るときは引数はany、ないしはボトム型にしないといけない
    • 引数は反変なので
type F = (a:number) => string
type R = ReturnType<F> // string

type MyReturnType<F> = F extends (a: any) => (infer R) ? R : void
type R2 = MyReturnType<F> // string

type MyReturnType2<F> = F extends (a: never) => (infer R) ? R : void
type R3 = MyReturnType2<F> // string

Escape Hatches

  • 多用せぬこと

    • 何かがおかしい証拠

Type Assertions

function formatInput(input: string) {
  // ...
}

function getUserInput(): string|number{
  // ...
}


const input = getUserInput()

formatInput(input) // Error: Argument of type 'string | number' is not assignable to parameter of type 'string'.

formatInput(input as string)

formatInput(<string>input)
  • tsxのことを考えると<>よりasのほうがよい
  • 危ないことができるので可能なら避ける
function addToList<T>(list: T[], item:T){
// ...
}

const list = [1,2,3]

addToList(list, 4)
addToList('hoge', 'hoge') // Error: Argument of type '"hoge"' is not assignable to parameter of type 'string[]'.
addToList('hoge' as any, 'hoge') // passes

Nonnull Assertions

type Dialog = {
  id?: string
}

function closeDialog(dialog: Dialog) {
  if (!dialog.id) {
    return
  }
  // here dialog.id is refined to string
  dialog.id

  setTimeout(() => {
    removeFromDOM(
      dialog,
      document.getElementById(dialog.id) // Error: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
    )
  })
}

function removeFromDOM(dialog: Dialog, element:Element) {
  element.parentNode.removeChild(element) // Error: Object is possibly 'null'.
  delete dialog.id
}
  • 潜在的なnullundefinedにまつわるエラー

    • dialog.id: setTimeoutのコールバックなので、他の誰かが書き換えている可能性があり、refinementが効かない
    • document.getElementById(id): 見つからなければnull
    • element.parentNode: undefinedの可能性がある
    • root
    • DOMに追加されていない
  • nullundefinedにまつわるエラーを黙らせる演算子!
type Dialog = {
  id?: string
}

function closeDialog(dialog: Dialog) {
  if (!dialog.id) {
    return
  }
  // here dialog.id is refined to string
  dialog.id

  setTimeout(() => {
    removeFromDOM(
      dialog,
      document.getElementById(dialog.id!)!
    )
  })
}


function removeFromDOM(dialog: Dialog, element:Element) {
  element.parentNode!.removeChild(element)
  delete dialog.id
}
  • 多く現れたら、それはリファクタリングすべき兆候
type VisibleDialog = {
  id: string
}

type DestroyedDialog = {}

type Dialog = VisibleDialog | DestroyedDialog

function closeDialog(dialog: Dialog) {
  if (!('id' in dialog)) {
    return
  }
  // dialog is refined to VisibleDialog
  dialog.id

  setTimeout(() => {
    removeFromDOM(
      dialog,
      document.getElementById(dialog.id)!
    )
  })
}


function removeFromDOM(dialog: VisibleDialog, element:Element) {
  element.parentNode!.removeChild(element)
  delete dialog.id
}
  • ビックリを減らせた

Definite Assignment Assertions

let userId: string

fetchUser()

userId.toUpperCase() // Error: Variable 'userId' is used before being assigned.

function fetchUser() {
  userId = globalCache.get('userId')
}
  • これもビックリで黙らせられる
let userId!: string

fetchUser()

userId.toUpperCase()

function fetchUser() {
  userId = globalCache.get('userId')
}

Simulating Nominal Types

IS TYPESCRIPT’S TYPE SYSTEM STRUCTURAL OR NOMINAL?

  • 構造的型付け: 別の名前でも同じ形なら同じ型
  • ゆえに起きる問題:
type CompanyID = string
type OrderID = string
type UserID = string

const userId: UserID = '1234'
const companyId: CompanyID = userId // OK (!!!)
  • 公称型: 同じ形でも別の名前なら別の型

    • TSにおいて、このような型は他にenumがある
  • unique Symbolとの交差型で公称型をシミュレートすることができる
type CompanyID = string & { readonly brand: unique symbol }
type OrderID = string & { readonly brand: unique symbol }
type UserID = string & { readonly brand: unique symbol }

// Companion Object Pattern
function CompanyID(id: string): CompanyID { return id as CompanyID }
function OrderID(id: string): OrderID { return id as OrderID }
function UserID(id: string): UserID { return id as UserID }

const userId: UserID = UserID('1234')
const companyId: CompanyID = userId // Error: Type 'UserID' is not assignable to type 'CompanyID'.

const userIdAsString: string = userId // OK

Safely Extending the Prototype

  • Array.prototypeとかを拡張する
function tuple<T extends unknown[]>(...ts: T): T {
  return ts
}

interface Array<T> {
  zip<U>(list: U[]): [T, U][]
}

Array.prototype.zip = function <T, U>(
  this: T[],
  list: U[]
): [T, U][] {
  return this.map((v, k) => tuple(v, list[k]))
}

const a = [1, 2, 3]
const b = ['a', 'b', 'c']
const c = a.zip(b) // [number,string][]
  • interface merging言語仕様のおかげでArray.prototypeが拡張される

    • typeだとむり
  • moduleモードのファイルでdeclare globalしてもよい

zip.ts

export {}

function tuple<T extends unknown[]>(...ts: T): T {
  return ts
}

Array.prototype.zip = function <T, U>(
  this: T[],
  list: U[]
): [T, U][] {
  return this.map((v, k) => tuple(v, list[k]))
}

declare global {
  interface Array<T> {
    zip<U>(list: U[]): [T, U][]
  }
}
  • ただし、このままだとimportしなくても動作してしまう
const a = [1, 2, 3]
const b = ['a', 'b', 'c']
const c = a.zip(b) // OK
  • importを強制するためにはexcludeする

tsconfig.json

  "exclude": [
    "src/zip.ts"
  ]
const a = [1, 2, 3]
const b = ['a', 'b', 'c']
const c = a.zip(b) // Error: Property 'zip' does not exist on type 'number[]'.
import './zip'

const a = [1, 2, 3]
const b = ['a', 'b', 'c']
const c = a.zip(b) // OK

Exercise

union typesの排他的論理和版をつくる

type ExclusiveUnion<T, U> = Exclude<T | U, T & U>

type A = ExclusiveUnion<1 | 2 | 3, 2 | 3 | 4>
const a1: A = 1
const a2: A = 2 // Error
const a3: A = 3 // Error
const a4: A = 4