型にハマらない TypeScript(仮)

公開:2020/09/18
更新:2020/09/18
6 min読了の目安(約3900字TECH技術記事

この記事について

こちらのイベントで LT した内容のコードです。
Webナイト宮崎 Vol.10 ~てげTypeScriptを学びたい~ - connpass

はじめに

環境

$ deno --version
deno 1.3.3
v8 8.6.334
typescript 4.0.2

README.ts

'自己紹介'
// いつもの自己紹介に必要なプロパティ
interface SelfIntroduction {
  name: string
  job: string
}
// Webナイト宮崎専用のプロパティ
interface SelfIntroduction {
  宮崎の好きなもの: string[]
}

'in Webナイト宮崎 Vol.10 ~てげTypeScriptを学びたい~'

const me: SelfIntroduction = {
  name: 'nunulk',
  job: 'フリーランスのウェブ開発者',
  宮崎の好きなもの: ['辛麺', 'スコール', '青島'],
}

console.log(me)

any vs. unknown

// import ky from 'https://deno.land/x/ky/index.js';

// const { data } = await ky.post('https://postman-echo.com/post', { json: { id: 1, name: 'John' } }).json()

// name を neme に typo すると、仮引数の型に any を指定した場合、コンパイルエラーにならない
const data = { id: 1, neme: 'John' }

/*
  any な人に挨拶する
*/
const greetToAny = (x: any) => {
  console.log('name in any', { name: `Hello, ${x.name}!` })
}

const anyPerson: any = data

greetToAny(anyPerson)

/*
  unknown な人に挨拶する
*/

type Person = {
  id: number
  name: string
}

const hasName = (arg: unknown): arg is { name: string } => {
  return (arg as { name: string }).name !== undefined
}

const isPerson = (arg: unknown): arg is Person => {
  return typeof arg === 'object'
  && typeof (arg as Person).id === 'number'
  && typeof (arg as Person).name === 'string'
}

// x.name が存在する保証がないのでコンパイルできない
// const greetToUnknown = (x: unknown) => {
//   console.log('name in unknown', { name: `Hello, ${x.name}!` })
// }

// name があることを保証するコードを書けば、タイポも発見することができる
const greetToUnknown = (x: unknown) => {
  if (isPerson(x) || hasName(x)) {
    console.log('name in unknown', { name: `Hello, ${x.name}!` })
  } else {
    console.error('[Error] x is not a person!')
  }
}

const unknownPerson: unknown = data

greetToUnknown(unknownPerson)

the price of freedom?


/*
The price of freedom is death. - Malcolm X
(自由の代償は死だ ― マルコムX)
*/

/*
TypeScript における自由とは...
_人人人_
> any <
 ̄Y^Y^Y^ ̄
*/

namespace Anytyped {
  const isDeath = (o: any, fn: any) => {
    try {
      return fn(o) === undefined
    } catch (_e) {
      return true
    }
  }

  const priceOf = (freedom: any) => freedom.price

  console.log('==== Anytyped ====')
  console.log('The price of freadom is ')

  // コンパイルエラーにならずに death! と出力される
  const freedom = 'freedom'
  if (isDeath(freedom, priceOf)) {
    console.log('death!')
  } else {
    console.log('priceless!')
  }
}

namespace Typed {
  type Freedom = {
    price: number
  }

  const isDeath = <T, U>(o: T, fn: (x: T) => U): boolean => {
    try {
      return fn(o) === undefined
    } catch (_e) {
      return true
    }
  }

  const priceOf = (freedom: Freedom) => freedom.price

  console.log('==== Anytyped ====')
  console.log('The price of freadom is ')

  // コンパイルエラー
  // const freedom = 'freedom'
  const freedom = { price: 0 }
  if (isDeath<Freedom, number>(freedom, priceOf)) {
    console.log('death!')
  } else {
    console.log('priceless!')
  }

Union types + never

/*
ユニオン型と never の便利な使い方
*/

const orderStatues = {
  Ordered: 'ordered',
  Shipping: 'shipping',
  Shipped: 'shipped',
	// この行を追加すると switch 文のところでエラーになる
	// Paid: 'paid',
} as const

type OrderStatus = typeof orderStatues[keyof typeof orderStatues]

type Order = {
  readonly id: number
  status: OrderStatus
}

const canCancel = (status: OrderStatus): boolean => {
  switch (status) {
    case 'ordered':
    case 'shipping':
      return true
    case 'shipped':
		// case 'paid': この行が必要
      return false
    default:
      const _: never = status
      console.error(`invalid status: ${_}`)
      return false
  }
}

const o1 = { id: 1, status: orderStatues.Ordered }
console.log('can cancel ordered item? ', canCancel(o1.status))

const o2 = { id: 1, status: orderStatues.Shipped }
console.log('can cancel shipped item? ', canCancel(o2.status))

おわりに

コードに間違いなどあれば、ご指摘ください 🙏