ミツモア Tech blog

「ミツモア」を運営する株式会社ミツモアの技術ブログです

型トリビアの泉〜素晴らしき型知識〜

※ こちらはミツモアAdvent Calendar 2023の13日目の記事です。

型知識が明日のあなたを変える、型トリビアの泉へようこそ

ミツモアの坂本です。

本日も、生きていく上で役には立ちそうだし、つい人に教えたくなってしまうような型トリビアを紹介していきます。

20へぇ: !を多用すると分かりやすくなる

TypeScriptではOptional型に対して存在チェックをせずにアクセスすると 'num' is possibly 'undefined'. といったようなエラーが出ます。

丁寧に書く場合は

const numStr = num?.toString() ?? '0'

のように書かないといけません。ただこのように書くと、URLのパラメーターからデータを取得する場合など実際には num が undefined になることがない場合にも ?? '0' を書かなければならず、存在しないケースなのにデフォルト値が存在するように見えてしまうデメリットもあります。

そんな時使えるのが ! (Non-Null assertion Operator) です。ただしこれはコンパイラを騙す行為でもあるため、バグの原因となる可能性もあります。そのため意図的に利用しているから注意してね!の意味を込めるために以下のように書くと良いです。

const numStr = num!!!!!!!!!!!.toString()

どうです?わかりやすいですよね!実はこれは連続して使っても問題ありません。

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator

40へぇ: inferを使って、型を盗める

TypeScriptには infer というものがあります。infer は推測する・割り出すというような意味があります。これは型の情報を横取りできる型で、少し複雑な型を書きたい時にとても便利です。

元から利用できる便利な型に ReturnType<T> がありますが、これは関数の返り値の型を取得する関数で、以下のように定義されています。

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

軽く解説すると、「Generics で T を取り、T(...args: any[]) => infer R の型に当てはまる場合にRを返し、それ以外は any (= 不明)として返す」という型です。

ここに出てくる infer R が大事で、 (...args: any[]) => any の関数型の返り値の部分を再利用したい場合はその部分に infer を使うことでキャプチャできます。

これはもちろん他の部分にも適用でき、引数の型を取得したければ以下のように記述できます。

type ArgumentType<T> = T extends (...args: infer A) => void ? A : any;

infer は Conditional Type の extends とセットでしか使えないため、次のように書くことはできない点に注意です。

type ReturnType<T extends (...args: any[]) => infer R> = R

https://www.typescriptlang.org/docs/handbook/type-inference.html

実際のプロダクト内での例

camelCaseからkebab-caseへ文字列変換を行う関数で利用しています。変換元の文字列がLiteral型だった場合型を失わないように型も変換する場合に利用しています。

type CamelToKebabCase<S extends string> =
  S extends `${infer T}${infer U}` ?
    `${T extends Capitalize<T> ? '-' : ''}${Lowercase<T>}${CamelToKebabCase<U>}`
  : S

const camelToKebab = <C extends string>(str: C): CamelToKebabCase<C> => {
  // ... 実際の処理は省略
}

const camel = 'camelCase'
const kebab = camelToKebab(camel) // 型も実際の値も 'camel-case' となる

60へぇ: & を使ってonから始まるものを抜き出せる

& はリテラル型に利用すると共通するものだけを取得することができます。

type Dog = { height: number; weight: number; tail: number }
type Human = { height: number; weight: number; stride: number }

// 'height' | 'weithg'
type CommonFields = keyof Dog & keyof Human
// これは❌
type CommonFields =  keyof (Dog & Human)

これを利用すると、Web開発をしている時に例えば onClick など on から始まるものだけを取得したかったりする場合がありますよね?(え?ない?あるんです)

その場合には Template Literal 型と組み合わせると便利です。

type HandlerKeys = keyof HTMLElement & `on${string}`
type EventHandlers = Pick<HTMLElement, HandlerKeys>
/**
 * {
 *  onfullscreenchange: ((this: Element, ev: Event) => any) | null;
 *  onfullscreenerror: ((this: Element, ev: Event) => any) | null;
 *  onabort: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
 *  ... 92 more ...;
 *  onwheel: ((this: GlobalEventHandlers, ev: WheelEvent) => any) | null;
 * }
 */

Template Literal Type については去年記事を書きましたのでそちらもぜひご覧ください!

engineering.meetsmore.com

80へぇ: クラスのgenericsに渡された型を取れる

何かベースとなるクラスがあり、そこにGenericsを利用して型を拡張している場合に、 BaseClass<Injected>Injected の型が欲しい時に便利な型です。シンプルではあるのですが、ちょっと思いつくのに時間がかかってしまったので紹介させてもらいます。

以下サンプルコード内のInjectedValue がそれに当たります。例を無理やり考えたので変なユースケースになっていますが、実際のプロダクトの中でベースクラスに対して継承して作られたクラスがいくつかあり、それらに対して共用できる関数を型安全に書くために役立ちました。具体的には請求書や見積書など、同じような内容だが目的は違う書類をいくつか取り扱う場面でこのような型が役に立ちました。

class BaseObj<T> {
  constructor(public _value: T) {}

  setValue(value: T): void {
    this._value = value
  }
}

// BaseObjのTの型を取得する
type InjectedValue<T extends BaseObj<any>> = T extends BaseObj<infer P> ? P: never

// Dogはheightが文字列
class Dog extends BaseObj<{ height: 'tall' | 'low';  }> {}
// Humanはheightが数値
class Human extends BaseObj<{ height: number;  }>  {}

const setHeight = <O extends BaseObj<any>>(obj: O, height: InjectedValue<O>['height']) => {
  obj.setValue(height)
}

const dog = new Dog({ height: 'low' })
const human = new Human({ height: 180 })
// それぞれのクラスに合わせた型を受け取れる
setHeight(dog, 'tall')
setHeight(human, 160)

97へぇ: そんなことをやってくれる型を集めたライブラリ 、type-fest がある

自分で頑張って型を作らなくても大体やりたいことはこのライブラリが実現してくれていますのでこれを利用すると幸せな型ライフが送れると思います!

https://github.com/sindresorhus/type-fest

他にも ts-essentialstypical といった類似ライブラリもありますが type-fest がダントツでダウンロード数が多いです。

98へぇ: type-challengesをやると、楽しいし勉強になる

type-challengesという型の問題集のようなリポジトリがあり、これを解くことでTypeScriptの型のいい勉強になります。TypeScriptを始めたばかりという方はまずeasyを解けるようになればいいスタートを切れるのではないでしょうか。mediumあたりまでは一通り目を通してみると良いと思います。それ以上のレベルはもはや型でやらなくて良いのでは?といった趣味レベルの型も登場してきますので、型パズルとして楽しめます

https://github.com/type-challenges/type-challenges

もう少しクリアしていってる感が欲しい…!という方には https://typehero.dev/ がおすすめです。かっこいいUIと共に似たような問題を解いていくことができます。

皆さんも型マスターになって型破りなTypeScriptエンジニアになりましょう!

99へぇ: ミツモアは、エンジニアを大募集している

ミツモアでは様々な職種のエンジニアを積極的に募集しています。
ご興味がある方はぜひ気軽にお話ししましょう!