1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

1人フロントエンドAdvent Calendar 2022

Day 22

TypeScriptのユーティリティ型を再実装する Part3

Last updated at Posted at 2022-12-21

はじめに

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>;

SampleReturnTypestring | 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>;

Sample01Rectangleになります。InstanceTypeはコンストラクタの帰り値から取得します。つまりこの関数はParametersConstructorParametersReturnTypeときたので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で取得できるものだけになっていますね。ParametersReturnTypeを活かして以下のように作ることもできます。

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とほとんど同じですが、ThisParameterTypethisを持っていないか確認しています。ない場合は何もせずにすぐ返しています。これによるメリットはいまいちわかりませんでした(関数を作り直すことは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())
    }
  },
});

MakeFunctionObjectdataに値を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;
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?