はじめに
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;