Utility Typesで楽に、Type Transformしよう

f:id:yun8boo:20200624151226p:plain

こんにちは。フロントエンドエンジニアの渡邉です。 普段ReactとTypeScriptを書いています。 今回はTypeScriptのUtility Typesについて紹介します。 記事のタイトルが某 大柴さんみたいになっていますが、この記事を読んだ方の力に少しでもなれたら幸いです。

目次

  • Utility Types
  • よく使うUtility Types
  • その他Utility Types
  • 最後に

Utility Types

公式ドキュメント

Utility Typesは楽にType Transformするための型で、TypeScriptによって提供されています。

  • Partial<T>
  • Readonly<T>
  • Record<K,T>
  • Pick<T,K>
  • Omit<T,K>
  • Exclude<T,U>
  • Extract<T,U>
  • NonNullable<T>
  • Parameters<T>
  • ConstructorParameters<T>
  • ReturnType<T>
  • InstanceType<T>
  • Required<T>
  • ThisParameterType
  • OmitThisParameter
  • ThisType<T>

よく使うUtility Types

以下のUtility Typesは個人的によく使うので説明と実際の使用例を紹介します。

  • Pick
  • Omit
  • Parameters
  • ReturnType
Pick<T,K>

Tに渡した型から指定のプロパティを抽出した型に変換します。

type Hoge = {
  hoge: string
  foo: number
}

type PickFoo = Pick<Hoge, | 'foo'>
/*
type PickFoo = {
  foo: number
}
*/

Pickをよく使う理由として、ある型の一部分だけが必要な型を作成する場面がよく出てくるからです。 例えば下記コードのようなUser型があり、その中でnameとgenderだけを使う型が欲しい時に使います。

interface User {
  id: string
  name: string
  gender: string
  email: string
  birthday: Date
}

type ViewUserInfo = Pick<User, 'naem' | 'gender'>
Omit<T,K>

Tに渡した型から指定のプロパティを除去した型に変換します。

type Hoge = {
  hoge: string
  foo: number
}

type OmitFoo = Omit<Hoge, | 'foo'>
/*
type OmitFoo = {
  hoge: string
}
*/

OmitはPickとは逆に、ある型の一部分だけがいらない型を作成するときに使います。 User型からgenderだけ使わない型が必要な場合。

interface User {
  id: string
  name: string
  gender: string
  email: string
  birthday: Date
}

type ViewUserInfo = Omit<User, 'gender'>
Parameters<T>

Tに渡した関数の引数の型をタプルとして抽出した型にします。

type Hoge = {
  hoge: string
  foo: number
}

const hogeFunc = (arg: Hoge) => {
  console.log(arg)
}

type ParametersHoge = Parameters<typeof hogeFunc>
/*
  type HogeParameters = [Hoge]
*/

postでリクエストする場面を例に説明します。 型ファイル(types.ts)とapi呼び出し関数をまとめたファイル(apis.ts)、実際に実行するファイル(example.ts)に分けています。 example.tsをまずParametersを使わず実装してみます。

types.ts

export interface PostData {
  name: string
  email: string
}

apis.ts

import { PostData } from './types'
import axios from 'axios'

// PostData型の引数を取る
export const postData = async(data: PostData) => {
  await axios('/users', data)
  return 'success'
}

example.ts

import { postData } from './apis'

postData({ 
  name: '太郎',
  email: 'hoge@hoge.com'
}).then(res => {
  // ...処理
})

上のコードの様に引数に直接objectをを渡すと可読性が落ちるので、引数に渡す用の変数を宣言します。 ただその時点では型が分からないため補完が効かないのと、dataに何が必要なのか分かりません。

import { postData } from './api'

const data = { 
  name: '太郎',
}

// dataのプロパティに漏れがあった場合ここで気付きます。
postData(data).then(res => {
  // ...処理
}) 

なので、types.tsからPostData型をimportしてきても良いのですが、postDataがすでにimportされているので、Parametersを使って変数dataをpostDataのパラメータの型にします。

example.ts

import { postData } from './apis'
// 第一引数の型がほしいので[0]で抽出しています。
const data: Parameters<typeof postData>[0] = { 
  name: '太郎',
  email: 'hoge@hoge.com'
}

postData(data)

直感的に「postData関数のパラメータの型だ」ということが分かるので良いです。

ReturnType<T>

関数の返り値の型を返します。

type Hoge = {
  hoge: string
  foo: number
}

const hogeFunc = (arg: Hoge) => {
  return arg.hoge
}

type ReturnTypeHoge = ReturnType<typeof hogeFunc>
/*
  type ReturnTypeHoge = string
*/

redux-sagaを使って開発しているときに自分はReturnTypeを使っています。

redux-sagaについて知りたい方はスタメンでもこちらの記事で紹介しています。

redux-saga/effectsselectを使いstateからnameInputValueemailInputValue抽出してそのデータをaxiosを使いpostする例で紹介します。

前提条件 - redux-sagaを使うための設定等は省いて、実際に使用するコードの部分だけ記載 - input要素からonChangeイベントでnameInputValueemailInputValueがstateにセットされている

const POST_DATA = 'POST_DATA';

interface AppState {
  user: UserState,
  // その他state
}

interface UserType {
  id: string
  name: string
  email: string  
}

interface UserState {
  users: UserType[]
  nameInputValue: string // input value state
  emailInputValue: string // input value state
}

interface PostData {
  name: string,
  email: string
}

// action
const postDataAction = (id: string) => ({
  type: POST_DATA
});

// api呼び出し関数
export const postData = async(data: PostData) => {
  try{
    await axios.post('/users', data)
    return {payload: 'success'}
  }catch {
    return {error: 'error'}
  }
}

// セレクター
const userSelector = (state:AppState) => state.user;

// saga task
function* runPostData(){
  // ReturnType<typeof userSelector> 型変数を宣言して、代入時に型チェックする
  const { nameInputValue, emailInputValue }: ReturnType<typeof userSelector> = yield select(userSelector)
  // ここでも Parameters<typeof postData> 型変数を宣言して、代入時に型チェックする
  const data: Parameters<typeof postData> = {
    name: nameInputValue
    email: emailInputValue
  }
  const { payload, error }: { payload?: string, error?: string } = yield call(CompassNoteAPI.requestCompassNote, params)
  if(payload && !error){
    yield put(...) // 成功時の処理
  }else{
    yield put(...) // 失敗時の処理
  }
}

// saga task
function* handlePostData() {
  yield takeEvery(POST_DATA, runPostData)
}

yieldを使用して変数に代入すると型推論でany型になってしまうため、ReturnTypeを使い型を指定しています。

その他Utility Types

Partial<T>

Tに渡した型のプロパティを全て省略可能にします。

type Hoge = {
  hoge: string
  foo: number
}
type PartialHoge = Partial<Hoge>

/*
PartialHoge = {
  hoge?: string
  foo?: number
}
*/
Readonly<T>

Tに渡した型のプロパティを全てreadonlyにして再代入不可にします。

type Hoge = {
  hoge: string
  foo: number
}
type ReadonlyHoge = Readonly<Hoge>

/*
type ReadonlyHoge = {
  readonly hoge: string
  readonly foo: number
}
*/
Record<K,T>

Kに渡した型がプロパティとなりTがそのプロパティの型になります。

interface Hoge {
  title: string;
}

type Foo = 'home' | 'about' | 'contact';

type RecordHoge = Record<Foo, Hoge> 
/*
type RecordHoge = {
  home: Hoge;
  about: Hoge;
  contact: Hoge;
}
*/
Exclude<T,U>

Tに渡した型から、Uの型を除去した型に変換します。

type Hoge = {
  hoge: string
  foo: number
}

type Bar = {
  bar: boolean
}

type ExcludeHoge = Exclude<Hoge | Bar, Hoge>
/*
type ExcludeHoge = {
  bar: boolean
}
*/
Extract<T,U>

Tに渡した型から、Uの型を抽出した型に変換します。

type Hoge = {
  hoge: string
  foo: number
}

type Bar = {
  bar: boolean
}

type ExtractHoge = Extract<Hoge | Bar, Hoge>
/*
type ExtractHoge = {
  hoge: string
  foo: number
}
*/
NonNullable<T>

T型からnullとundefinedを除外した型にします。

type Hoge = {
  hoge: string | undefined
  foo: number | null
}

type NonNullableHoge = NonNullable<Hoge>
/*
type NonNullableHoge = {
    hoge: string;
    foo: number;
}
*/

これには注意点が一つあって、optionalのプロパティは除去されないです。

type Hoge = {
  hoge?: string
  foo: number | null
}

type NonNullableHoge = NonNullable<Hoge>
/*
type NonNullableHoge = {
    hoge?: string;
    foo: number;
}
*/

ConstructorParameters<T>

Parametersのコンストラクター版です。

class Hoge {
  constructor(a: string, b: number) {}
}

type ConstructorParametersHoge = ConstructorParameters<typeof Hoge>
/*
  ConstructorParametersHoge = [string, number]
*/

InstanceType<T>

型Tのコンストラクタの返り値の型を返します。

class Hoge {
  constructor(a: string, b: number) {}
}

type InstanceTypeHoge = InstanceType<typeof Hoge>
/*
  type InstanceTypeHoge = Hoge
*/

Required<T>

型Tの省略可能のプロパティを必須にします。

type Hoge = {
  hoge?: string
  foo?: number
}

type RequiredHoge = Required<Hoge>
/*
  type RequiredHoge = {
    hoge: string;
    foo: number;
  }
*/

ThisParameterType

thisのパラメータを取得します。(使い所がわからない)

function toHex(this: Number) {
  return this.toString(16);
}

function numberToString(n: ThisParameterType<typeof toHex>) {
  return toHex.apply(n);
}

OmitThisParameter

thisのパラメーターを削除します。(使い所がわからない)

注:--strictFunctionTypesが有効になっている場合にのみ正しく機能します。

function toHex(this: Number) {
    return this.toString(16);
}

// The return type of `bind` is already using `OmitThisParameter`, this is just for demonstration.
const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);

ThisType<T>

objectの中のthisを型Tにします。

interface Hoge {
  hoge: string;
}
interface Foo {
  foo(): void;
}

// objの型はFooであり、obj内でのthisの型はHogeと明示的に指定します
const obj: Foo & ThisType<Hoge> = {
  foo() {
    console.log(this.hoge); // undefined
  },
};

最後に

実際に触ってみてUtility Typesはとても便利ですが、公式ドキュメントの見つけづらいところにあります。 今回の記事で少しでも多くの人に知ってもらえれば幸いです。

株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひエンジニア採用サイトをご覧ください。