全集中 TypeScriptの呼吸 壱ノ型Mapped Types、弐の型Conditional Types、そして12の型のその先 type-fest
例えば
class Dog {
name: string
age: number
constructor (dogData: any) {
this.name = dogData?.name
this.age = dogData?.age
}
cry = () => {
console.log(`I’m ${this.name}`)
}
}
みたいなクラスがあった時に、constructorで受け取る型として
{
name: string
age: number
}
みたいなのが欲しくなる事があります。筋力で解決するなら
type DogData = {
name: string
age: number
}
const dataIsDogData = (data: unknown): data is DogData => {
const d = data as DogData
if (typeof d?.name !== 'string') return false
if (typeof d?.age !== 'number') return false
return true
}
class Dog {
name: string
age: number
constructor(data: unknown) {
if (!dataIsDogData(data)) throw new Error('unknown data')
this.name = data.name
this.age = data.age
}
cry = () => {
console.log(`I’m ${this.name}`)
}
}
みたいになると思います。でもプロパティが10個の場合とかを想像するとちょっと筋肉が身震いしちゃいますよね。Proto3やGraphQL Queryを元にgenerateされているなら別に人力で書く必要のない型(IDLを書く手間はある)なんですが、そういう手法が取れない事もあるかもしれません。
そんな時の選択肢の1つはUtility Typesを利用する事です。TypeScriptにはUtility Typesと呼ばれる便利な型がいくつか標準で搭載されています。例えばその中の1つであるPickを使う事で
type DogData = Pick<Dog, 'name' | 'age'>
といった風に書く事もできます。
でもプロパティ名を10個も羅列しないといけないのは、筋力に自信が無い人だとまだちょっと不安が残りますよね。犬だけじゃなく猫も増えるかもしれないし、10個のプロパティのうちnameだけが無くて代わりにserialNumberを持っているRobotの取り扱いを始める可能性だってあります。叶うならDogやCatやRobotからFunction型だけ取り除いた型を手に入れられないか。
そんな時の選択肢を広げてくれるのがMapped TypesとConditional Typesです。
type DogData = {
[Key in keyof Dog]: Dog[Key] extends Function ? never : Dog[Key]
}
制御構造みたいな見た目ですね。この中の
{[Key in keyof Dog]: Hoge}
のfor in構文みたいな部分はMapped Types
Dog[Key] extends Function ? never : Dog[Key]
の三項演算子みたいな部分はConditional Typesと呼ばれているものです。
壱ノ型Mapped Types
Mapped Typesはその型の各プロパティの型に対して何らかの操作を加えた新たな型を返します。(keyof Tを渡す使い方なら)
例えばUtility TypesのPickやReadonly等もMapped Typesを利用して作られた型です。
type ReadonlyDog1 = Readonly<Dog>
は、敢えてUtility Typesや型変数を使わずに書き直すなら
type ReadonlyDog2 = {
readonly [Key in keyof Dog]: Dog[Key]
}
type ReadonlyDog3 = {
readonly [Key in 'name' | 'age' | 'cry']: Dog[Key]
}
のように書くこともできます。これは元となった型の各プロパティに対してreadonlyを与えた型を返しています。また必ずしも既存の型をベースにする必要もなくて
type Meta = {
[Key in 'title' | 'description'] : string
}
と書くのは
type Meta = {
title: string
description: string
}
と書くのと等価です。
弐の型Conditional Types
三項演算子です。
type Result = Pig extends Eatable ? Pork : Pet
type Eatable = {
grade: number
}
type Pork = {
grade: number
}
type Pet = {
name: string
}
という型に対してもしもPigが
type Pig = {
grade: number
}
だった場合にはResultは
type Result = {
grade: number
}
になります。
Mappded TypesとConditional Typesを組み合わせる
type DogData = {
[Key in keyof Dog]: Dog[Key] extends Function ? never : Dog[Key]
}
type DogData = {
[Key in keyof Dog]: Dog[Key] extends Function ? never : Dog[Key]
}
これらをまとめると「Dogの各プロパティ値の型に対してFunction型(に代入可能)ならnever型を、そうでなければそのプロパティ値の型をそのまま返す」型になります。never型というのは、触るとコンパイラに殺されるので触られていない事がコンパイラによって保証される型です。
そして、これをDog型とFunction型の組み合わせ以外でも使えるよう型変数TとCに置き換えると
type FilterNot<T, C> = {
[Key in keyof T]: T[Key] extends C ? never : T[Key]
}
type NonFunctionKeysOnly<T> = FilterNot<T, Function>
type DogData = NonFunctionKeysOnly<Dog>
type CatData = NonFunctionKeysOnly<Cat>
type RobotData = NonFunctionKeysOnly<Robot>
こうじゃ。
だいぶ虫様筋と外眼筋に優しくなりました。ここからneverを除いた型にしたかったり、さらに楽をしたければ、例えばtype-festみたいな型ライブラリを使うのもいいでしょう。例えばtype-festのConditionalExceptを使うと
import { ConditionalExcept } from 'type-fest'
type NonFunctionKeysOnly<T> = ConditionalExcept<T, Function>
type DogData = NonFunctionKeysOnly<Dog>
type CatData = NonFunctionKeysOnly<Cat>
type RobotData = NonFunctionKeysOnly<Robot>
と書く事ができます。この例に限らず、ある型を元に別の型を得たい場合や、Primitive型ほしくない?となった時などにはあると便利です。
Author And Source
この問題について(全集中 TypeScriptの呼吸 壱ノ型Mapped Types、弐の型Conditional Types、そして12の型のその先 type-fest), 我々は、より多くの情報をここで見つけました https://qiita.com/oubakiou/items/41ff72a70ccfdb5d368d著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .