勉強日記

チラ裏

Programming TypeScript ch6 (2/3) Advanced Function Types

www.oreilly.com


Totality

  • switch-caseの漏れとかを教えてくれるやつ
type Weekday = 'Mon' | 'Tue' | 'Wed'|'Thu'|'Fri'
type Day = Weekday | 'Sat' | 'Sun'

// Error: Function lacks ending return statement and return type does not include 'undefined'.
function getNextDay(w: Weekday): Day {
  switch(w) {
    case 'Mon': return 'Tue'
  }
}

TSC Flag: noImplicitReturns

  • tsconfigで"noImplicitReturns": trueしておくと、下記のような潜在的な不具合を検出できる
// Error: Not all code paths return a value.
function isBig(n: number) {
  if (n >= 100) {
    return true
  }
}

Advanced Object Types

Type Operators for Object Types

type APIResponse = {
  user: {
    userId: string,
    friendList: {
      count: number,
      friends: {
        firstName: string,
        lastName: string
      }[]
    }
  }
}

function getAPIResponse(): Promise<APIResponse> {
  // ...
}

function renderFriendList(friendList: any) { // どうする?
  // ...
}

const response = await getAPIResponse()
renderFriendList(response.user.friendList)
  • friendListのtypehintをどうする?
    • とりあえずanyでスタブにしている
  • FriendListを定義する?
type FriendList = {
  count: number,
  friends: {
    firstName: string,
    lastName: string
  }[]
}

type APIResponse = {
  user: {
    userId: string,
    friendList: FriendList
  }
}

function getAPIResponse(): Promise<APIResponse> {
  // ...
}

function renderFriendList(friendList: FriendList) {
  // ...
}

const response = await getAPIResponse()
renderFriendList(response.user.friendList)
  • いちいち名前をつけたくないこともある
type APIResponse = {
  user: {
    userId: string,
    friendList: {
      count: number,
      friends: {
        firstName: string,
        lastName: string
      }[]
    }
  }
}

function getAPIResponse(): Promise<APIResponse> {
  // ...
}

function renderFriendList(friendList: APIResponse['user']['friendList']) {
  // ...
}

const response = await getAPIResponse()
renderFriendList(response.user.friendList)

The keyof operator

type APIResponse = {
  user: {
    userId: string,
    friendList: {
      count: number,
      friends: {
        firstName: string,
        lastName: string
      }[]
    }
  }
}

type ResponseKeys = keyof APIResponse // 'user'
type UserKeys = keyof APIResponse['user'] // 'userId'|'friendList'
type FriendListKeys = keyof APIResponse['user']['friendList'] // 'count'|'friends'
  • 用法
type Vector3D<T = number> = {
  x: T,
  y: T,
  z: T
}

function get<T>(vector3d: Vector3D<T>, key: keyof Vector3D<T>): T {
  return vector3d[key]
}

const v: Vector3D = {
  x: 1,
  y: 2,
  z: 3,
}
const x = get(v, 'x')
const y = get(v, 'y')
const z = get(v, 'z')
const w = get(v, 'w') // Error: Argument of type '"w"' is not assignable to parameter of type '"x" | "y" | "z"'.

TSC Flag: keyofStringsOnly

  • keyofはデフォルトで number|string|symbolを返す
  • 配列の例
type KeysOfArray<T> = keyof T[] // number|'length'|'toString'|...
type NumericKeysOfArray<T> = number & keyof T[] // number
  • stringだけにしたい場合、tsconfigで"keyofStringsOnly": trueする
type KeysOfArray<T> = keyof T[] // 'length'|'toString'|...
type NumericKeysOfArray<T> = number & keyof T[] // never

The Record Type

  • nextDayをswitch-caseじゃなくてルックアップテーブルで書き換えたいぞ、になったとき
  • totalityの担保のために使える組み込みの型
type Weekday = 'Mon' | 'Tue' | 'Wed'|'Thu'|'Fri'
type Day = Weekday | 'Sat' | 'Sun'

// Error: Type '{ Mon: "Tue"; }' is missing the following properties from type 'Record<Weekday, Day>': Tue, Wed, Thu, Fri
let nextDay: Record<Weekday, Day> = {
  Mon: 'Tue'
}
let nextDay: Record<Weekday, Day> = {
  Mon: 'Tue',
  Tue: 'Wed',
  Wed: 'Thu',
  Thu: 'Fri',
  Fri: 'Sat'
}

Mapped Types

  • Record Typesよりももっと強力なやつ
type Weekday = 'Mon' | 'Tue' | 'Wed'|'Thu'|'Fri'
type Day = Weekday | 'Sat' | 'Sun'

// 3: typescript: Property 'Fri' is missing in type '{ Mon: "Tue"; Tue: "Wed"; Wed: "Thu"; Thu: "Fri"; }' but required in type '{ Mon: Day; Tue: Day; Wed: Day; Thu: Day; Fri: Day; }'.
let nextDay: {[Key in Weekday]:Day} = {
  Mon: 'Tue',
  Tue: 'Wed',
  Wed: 'Thu',
  Thu: 'Fri',
}
  • 既存の型のoptionalやreadonlyを付け外ししたりできる
type Account = {
  id: number,
  isEmployee: boolean,
  notes: string[]
}

type OptionalAccount = {
  [K in keyof Account]?: Account[K]
}

type NullableAccount = {
  [K in keyof Account]: Account[K] | null
}

type ReadonlyAccount = {
  readonly [K in keyof Account]: Account[K]
}

// equivalent to Account
type Account2 = {
  [K in keyof OptionalAccount]-?: OptionalAccount[K]
}

// equivalent to Account
type Account3 = {
  -readonly [K in keyof ReadonlyAccount]: ReadonlyAccount[K]
}

Built-in mapped types

type Account = {
  id: number,
  isEmployee: boolean,
  notes: string[]
}

type OptionalAccount = Partial<Account>

type ReadonlyAccount = Readonly<Account>

// equivalent to Account
type Account2 = Required<OptionalAccount>

type AccountId = Pick<Account, 'id'>
// { id: number; }

Companion Object Pattern

type Currency = {
  unit: 'EUR' | 'GBP' | 'JPY' | 'USD'
  value: number
}

const Currency = {
  from(value: number, unit: Currency['unit'] = 'USD'): Currency {
    return { unit, value }
  }
}
  • 型と値の名前空間は異なるので、同名にできる
  • まとめてimportできるのが嬉しい

Currency.ts

export type Currency = {
  unit: 'EUR' | 'GBP' | 'JPY' | 'USD'
  value: number
}

export const Currency = {
  from(value: number, unit: Currency['unit'] = 'USD'): Currency {
    return { unit, value }
  }
}

index.ts

import { Currency } from './Currency'

const amountDue: Currency = {
  unit: 'JPY',
  value: 83733.10
}

const otherAmouhtDur = Currency.from(330, 'EUR')