🤝

もっとTypeScriptと仲良くなる 逆引き.ts

2023/01/11に公開

パッケージ側で定義されているもの・自動生成されるものに乗っかっているだけでも十分に便利なTypeScript。ですが、思わぬエラーに悩まされる・エラーメッセージが読み解けなかったり、やりたいことを実現する手段がわからないこともしばしばあります。
そんな時に役立つなんやかんやが書いてある記事を目指して書きます。 ※随時アップデート

d.tsファイルとは

型定義ファイルです。アンビエント宣言というものを書くのに使われます。
例えばTypeScriptをインストールする際に付属している型定義ファイルlib.d.tsには windowdocumentなどのアンビエント宣言が書かれています。

アンビエント宣言 declare とは

JavaScriptに型情報を付加するための宣言です。JavaScriptで書かれたパッケージに型情報を定義するために利用されます。
ts ファイルに書いてしまっても動作しますが、 d.ts ファイルにまとめるのが一般的です。

packageVar = 'hoge'  // Cannot find name 'packageVar'.
declare var packageVar: string
packageVar = 'hoge'  // var packageVar: string

d.tsを利用する

// package.d.ts
declare var packageVar: string
packageVar = 'hoge'  // var packageVar: string

もっと詳しく
https://typescriptbook.jp/reference/declaration-file

型の定義方法 interfaceとtypeについて

概ねtype(型エイリアス)を使ってしまえばよいのではないでしょうか。

interfaceでできること → typeでできること

  • オブジェクトとクラスの型だけ定義できる → オブジェクト以外の型も定義できる
  • extendsキーワードによって拡張ができる → 交差型によって拡張できる
  • 同名の宣言で拡張される → エラーになる

interfaceの優位性

ライブラリ側でinterfaceを使用していると利用する側で好きに拡張が行えて便利。
一方でうっかり同名のinterfaceを宣言すると勝手に拡張されて困ることがありそう。

const todo: <ここの型> = {
  id: 'id',
  title: 'title',

  done: false
}

上記のtodoに適した型を、拡張を使いつつ実装するには以下のようなパターンがある。

interfaceの場合:

interface Todo {
  id: string
  title: string
}

interface TodoWithStatus extends Todo {
  done: boolean
}

もしくは

interface Todo {
  id: string
  title: string
}

interface Todo {
  done: boolean
}

typeの場合:

type Todo = {
  id: string
  title: string
}

type TodoWithStatus = Todo & {
  done: boolean
}

もっと詳しく
https://typescriptbook.jp/reference/object-oriented/interface/interface-vs-type-alias

特定の値だけを受け入れる型をつくりたい Enum, ユニオン型, オブジェクトリテラル

じゃんけんの手は3パターンしかない。stringでは広すぎる。

function battleJanken(hand: じゃんけんの手) {
  if (hand === 'グー')
  ...
}

Enum

中身はnumber。

enum Hand {
  Gu,
  Choki,
  Pa
}

function battleJanken(hand: Hand) {
  if (hand === Hand.Gu)
  ...
}

console.log(Hand.Gu) // 0
console.log(Hand.Choki) // 1
console.log(Hand.Pa) // 2

任意のstringを割り当てることもできる。

enum Hand {
  Gu = 'グー',
  Choki = 'チョキ',
  Pa = 'パー'
}

console.log(Hand.Gu) // グー
console.log(Hand.Choki) // チョキ
console.log(Hand.Pa) // パー

存在しない値や、割り当てたstring自体を指定するとエラーになる。

battleJanken(Hand.Rock) // Property 'Rock' does not exist on type 'typeof Hand'.
battleJanken('グー') // Argument of type '"グー"' is not assignable to parameter of type 'Hand'.

ユニオン型

すごくシンプル。

type Hand = 'gu' | 'choki' | 'pa'
battleJanken('choki')

ユニオンに含まれない値を指定するとエラーになる。

battleJanken('rock') // Argument of type '"rock"' is not assignable to parameter of type 'Hand'.

オブジェクトリテラル

const Hand = {
  Gu: 0,
  Choki: 1,
  Pa: 2
} as const

type HandType = typeof Hand[keyof typeof Hand];

function battleJanken(hand: HandType) {
  if (hand === Hand.Gu)
  ...
}

battleJanken(Hand.Gu)

関数に型をつけたい

引数と戻り値を明示したい。

文法は以下の通り。

function 関数名(引数名: 引数の型): 戻り値の型 {
  ...
}

const 関数名 = (引数名: 引数の型): 戻り値の型 => {
  ...
}

分割代入引数を使った具体例。型をインラインで書くとごちゃごちゃして読みづらくなりがち。

function hogeFunction({ hogeArgOne, hogeArgTwo }: { hogeArgOne: string, hogeArgTwo: number }): { hogeOne: string, hogeTwo: number } {
  ...
}

以下の内容と等価です。

type Argument = { hogeArgOne: string, hogeArgTwo: number }
type HogeFunctionResult = { hogeOne: string, hogeTwo: number }

function hogeFunction({ hogeArgOne, hogeArgTwo }: Argument): HogeFunctionResult {
  ...
}

戻り値が誤っていると、returnにエラーメッセージが表示される。

function hogeFunction({ hogeArgOne, hogeArgTwo }: Argument): HogeFunctionResult {
  return {
    hogeOne: 'sample',
  } // Property 'hogeTwo' is missing in type '{ hogeOne: string; }' but required in type 'HogeFunctionResult'.
}

関数を受け取る型を定義したい

以下のようにコールバック関数を受け取るときとか。

function 関数(処理後に実行する関数) {
  const result = await doSomething()
  処理後に実行する関数(result)
}

型の書き方は以下の通り。

type AfterDoSomething = (argument: string) => boolean

function 関数(処理後に実行する関数: AfterDoSomething) {
  const result: string = await doSomething()
  処理後に実行する関数(result)
}

変数の型を特定したい 型ガード

ユニオン型の変数は、typeofとif文を使えば型を特定できる。
URLのクエリパラメータでは取ってきた値が string | string[] になってたりするので、それを例にしてみる。

function useQueryParameter(): string | string[] {
  return 'dummyParameter'
}

// この時点ではsomeQueryParameterはstring | string[]
const someQueryParameter = useQueryParameter()

if (typeof someQueryParameter === 'string') {
  // someQueryParameterはstringになっている
}

ユーザー定義型ガード

「こういう条件ならこの変数はこの型だ!」というのを自前で定義できる。
戻り値を argument is someTypeにした関数でbooleanを返せばよい。

以下微妙なケースですが目を瞑ってください。

type User = {
  type: string
  name: string // diff
}
type Admin = {
  type: string
  id: number // diff
}

// is 演算子を使い、argument is typeを返す関数を定義する
function isUser(object: unknown): object is User {
  // booleanを返せばOK
  return object.name != null
}

const unknownObjectOfUser = {
  type: 'user',
  name: 'user name'
}
const unknownObjectOfAdmin = {
  type: 'admin',
  id: 1
}

if (isUser(unknownObjectOfUser)) {
  unknownObjectOfUser // {name: string, type: string}
}
if (!isUser(unknownObjectOfAdmin)) {
  unknownObjectOfAdmin // {type: string, email: string}
}

nullチェック

=== ではなく、 == null でnullとundefined両方チェックできる。
nullとundefinedを区別したい場合は === を使う。

const nullVar: string | null
const undefinedVar: string | undefined

nullVar != null // string確定
undefinedVar != null // string確定

プロパティの変更を防ぎたい readonly

readonlyが付与されたプロパティには再代入ができない。

type User = {
  readonly id: number
  name: string
}

const sampleUser: User = {
  id: 1,
  name: 'name'
}

sampleUser.id = 2 // Cannot assign to 'id' because it is a read-only property

執筆予定

  • never
  • ジェネリクス
  • 型のループ
  • 型の条件分岐

etc.

REF

TypeScript

TypeScript Deep Dive

普段はmofmofという会社でRailsやReact, GraphQLなどを取り入れたアジャイルな受託開発の仕事をしてます。
mofmof 開発チームレンタル

mofmof inc.

Discussion