はじめに
TypeScriptではよく行われる型変換をサポートするために、ユーティリティ型というものを提供しています。この記事ではそんなユーティリティ型を再実装してみようと思います。全部で21ケースありますが、1ケース(ThisType
)は単なる空のマーカーインターフェイスで、4ケースはintrinsic型(TypeScriptのコンパイラに隠された型)なので残りの16ケースを紹介します。この記事はPart3です。Part1はこちら、Part2はこちらです。
ConstructorParameters
ConstructorParameters
はコンストラクタのパラメータを取得する型です。Parameters
が関数だったのに対して、これはコンストラクタに作用する型です。縦横の長さを持つ長方形を彷彿させるようなクラスを考えます。
class Rectangle {
height: number;
width: number;
constructor(height: number, width: number) {
this.height = height;
this.width = width;
}
}
type Sample01 = ConstructorParameters<typeof Rectangle>;
この場合Sample01の型は[height: number, width: number]
となります。もし、コンストラクタが定義されていないクラスであればこの型は空配列を返します。実装はParameters
とほとんど同じです。
type MyConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
T
が(...args: any) => any
からabstract new (...args: any) => any
に変わりました。new
の部分でコンストラクタの部分を取ってます。abstruct
は抽象クラスにも対応するために追加されています。
これもTypeScriptの実装と同じです。
ReturnType
ReturnType
は関数の戻り値を取得する型です。
type SampleFunc = (arg1: string, arg2: number) => string | number;
type SampleReturnType = ReturnType<SampleFunc>;
SampleReturnType
はstring | number
になります。
Parameter
の返り値バージョンとなっています。Parameter
では無視した部分を取得することで型を作成することが出来ます。
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R
? R
: any;
Parameter
ではargsの部分を取得していた部分を返り値を取得するようにしました。改行を除くとTypeScriptと同じ実装になっています。
InstanceType
InstanceType
はインスタンス型を取得します。
class Rectangle {
height: number;
width: number;
constructor(height: number, width: number) {
this.height = height;
this.width = width;
}
}
type Sample01 = InstanceType<typeof Rectangle>;
Sample01
はRectangle
になります。InstanceType
はコンストラクタの帰り値から取得します。つまりこの関数はParameters
、ConstructorParameters
、ReturnType
ときたのでConstructorReturnType
に値します。
type MyInstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R
? R
: any;
改行を除くとTypeScriptと同じ実装になっています。
ThisParameterType
ThisParameterType
はその関数、メソッドが持つthis
の型を取得します。
type Sample01 =
ThisParameterType<(this: { height: number, width: number }, area: () => number) => void>
のようにするとthis
の部分だけ、つまり{ height: number, width: number }
が得られます。
ちなみにですが、Parameters
を使って
type Sample02 =
Parameters<(this: { height: number, width: number }, area: () => number) => void>
のようにするとthisの部分は得られず[area: () => number]
しか得られません。this
は他の引数とは異なり明示的に指定して取得する必要があるからです。つまり実装は以下のようになります。
type MyThisParameterType<T> =
T extends (this: infer U, ...args: any) => any
? U
: unknown;
...args
を置いておいてthis
から明示的に型を取得してそれを返すようにしています。
TypeScriptの実装ニノいて..args
の型はnever
になっています。ここだけnever
になっている理由はわからないです(実装はできるだけTypeScriptの実装に寄せてanyを使ってましたが、そもそも使わない型は全部neverでいいと考えています)。
OmitThisParameter
OmitThisParameter
は引数からthis
がないものとした型が返ってきます。
type Sample01 =
OmitThisParameter<(this: { height: number, width: number }, area: () => number) => void>
以上の例だと(area: () => number) => void
が返ってきます。引数がParameters
で取得できるものだけになっていますね。Parameters
とReturnType
を活かして以下のように作ることもできます。
type MyOmitThisParameter01<T> =
T extends (...args: any) => any
? (...args: Parameters<T>) => ReturnType<T>
: T;
他の型に依存したくないときは下のようにします。
type MyOmitThisParameter02<T> = T extends (...args: infer P) => infer R ? (...args: P) => R : T;
TypeScriptの実装では以下のようになっています(一部改行しています)。
type OmitThisParameter<T> =
unknown extends ThisParameterType<T>
? T
: T extends (...args: infer A) => infer R
? (...args: A) => R
: T;
MyOmitThisParameter02
とほとんど同じですが、ThisParameterType
でthis
を持っていないか確認しています。ない場合は何もせずにすぐ返しています。これによるメリットはいまいちわかりませんでした(関数を作り直すことはthisから値を抜き出すことに対してコストがかかるとかの理由があるんですかね?)。
おまけ
再実装しなかった型についても簡単に紹介します。
ThisType
ThisType
はこれまでのような渡された型からなんらかの型を生成するような型ではありません。この型は以下のように使います。
declare function MakeFunctionObject<Data, Method>(options: {
data: Data;
methods: Method & ThisType<Data & Method>;
}): Data & Method;
const rectangle = MakeFunctionObject({
data: { height: 8, width: 10 },
methods: {
area() {
return this.height * this.width;
},
outputArea() {
console.log(this.area())
}
},
});
MakeFunctionObject
はdata
に値をmethods
にそれらを扱うメソッドを用意するような関数をです。methods
ではdata
の値をthis
でアクセスしたり、methods
内で定義したメソッドにthis
でアクセスできるようになっています。このthis
の型を定義するのがThisType
です。
この例ではMakeFunctionObject
を利用してrectangle
を作っています。data
で縦幅と横幅の定義、methods
で面積とその出力する関数の定義を行なっています。この関数の場合Data
は{ height: number, width: number }
、Method
は{ area(): number, outputArea(): number }
となります。そしてThisType<Data & Methods>
となるので、methods
内のthis
は
{
height: number,
width: number,
} & {
area(): number,
outputArea(): number,
}
となります。このようにThisType
を用いてあるオブジェクト内で利用するthis
の型を決定することが出来ます。
なお、Method & ThisType<Data & Method>
となっていますが、ThisTypeが返す型は{}
なのでMethod
に影響はありません。
Uppercase
受け取った文字列を全て大文字にする型です。
type Sample01 = Uppercase<'✋hello'>;
type Sample02 = Uppercase<'✋HEllo'>;
type Sample03 = Uppercase<'✋HELLO'>;
これらは全て✋HELLO
型になります。
Lowercase
受け取った文字列を全て小文字にする型です。
type Sample01 = Lowercase<'✋hello'>;
type Sample02 = Lowercase<'✋HEllo'>;
type Sample03 = Lowercase<'✋HELLO'>;
これらは全て✋hello
型になります。
Capitalize
受け取った文字列の先頭を大文字にする型です。
type Sample01 = Capitalize<'hello'>;
この例ではHello
が得られます。
type Sample02 = Capitalize<'✋hello'>
では何も変わらず✋hello
が得られます。
この型はUppercase
を使って以下のように実装できます。
type MyCapitalize<S extends string> =
S extends `${infer T}${infer U}`
? `${Uppercase<T>}${U}`
: S;
UnCapitalize
受け取った文字列の先頭を小文字にする型です。
type Sample01 = UnCapitalize<'Hello'>;
この例ではhello
が得られます。
この型もLowercase
を用いて実装出来ます。
type MyCapitalize<S extends string> =
S extends `${infer T}${infer U}`
? `${Lowercase<T>}${U}`
: S;