TypeScript の Utility Types もどきを自分で実装する

Change log
9sako6
9sako6

TypeScript にはじめから用意されている Partial<Type>, Uppercase<StringType> 等の Utility Types, Intrinsic String Manipulation Types もどきを再実装して遊ぶ記事です。

実際の実装とは異なる場合があります。実際の実装を見たい方は、TypeScript で本物の Utility Types を書いてご確認ください。

なお、実際の ThisType<Type>, Uppercase<StringType>, Lowercase<StringType>, Capitalize<StringType>, Uncapitalize<StringType> の実装はコンパイラの中にあり、TypeScript のコードからは見えないのでご注意ください。本記事では、実用性を無視して TypeScript でどうにか再現しています。

#環境

  • TypeScript v4.6.2
    • "strict": true

#Partial<Type>

Type 型のプロパティを全て省略可能にした型を返します。

type MyPartial<Type extends Record<string, unknown>> = {
  [Key in keyof Type]?: Type[Key];
};

ちなみに、+ 演算子を使っても同じように修飾子を付与できます。ここでは特に使う必要はありません。

type MyPartial<Type extends Record<string, unknown>> = {
  [Key in keyof Type]+?: Type[Key];
};

#Required<Type>

Type 型のプロパティ全てを省略不可にした型を返します。

type MyRequired<Type extends Record<string, unknown>> = {
  [Key in keyof Type]-?: Type[Key];
};

#Readonly<Type>

Type 型のプロパティ全てを読み取り専用にした型を返します。

type MyReadonly<Type extends Record<string, unknown>> = {
  readonly [Key in keyof Type]: Type[Key];
};

ちなみに、逆に書き込み可能にする場合は - 演算子を使って readonly を削除します。

type Writable<Type extends Record<string, unknown>> = {
  -readonly [Key in keyof Type]: Type[Key];
};

#Record<Keys, Type>

キーの型が Keys、値の型がType であるオブジェクトの型を返します。

type MyRecord<Keys extends string | number | symbol, Type> = {
  [Key in Keys]: Type;
};

#Pick<Type, Keys>

Keys 型のプロパティだけを Type から取り出します。

type MyPick<Type extends Record<string, unknown>, Keys extends keyof Type> = {
  [Key in Keys]: Type[Key];
};

別解です。

type MyPick<Type extends Record<string, unknown>, Keys> = {
  [Key in keyof Type & Keys]: Type[Key];
};

#Exclude<UnionType, ExcludedMembers>

UnionType という合併型から、UnionType に代入可能な ExcludedMembers 型を除いた型を返します。 条件型の分配法則を利用します。

type MyExclude<UnionType, ExcludedMembers> = UnionType extends ExcludedMembers
  ? never
  : UnionType;

#Omit<Type, Keys>

Type 型から Keys 型のプロパティを除いた型を返します。 さっき作った MyExclude<UnionType, ExcludedMembers> を利用できます

type MyExclude<UnionType, ExcludedMembers> = UnionType extends ExcludedMembers
  ? never
  : UnionType;

type MyOmit<Type extends Record<string, unknown>, Keys> = {
  [Key in MyExclude<keyof Type, Keys>]: Type[Key];
};

#Extract<Type, Union>

Union 型に代入可能な Type 型を取り出します。 条件型の分配法則が光ります。

type MyExtract<Type, Union> = Type extends Union ? Type : never;

#NonNullable<Type>

Type 型を Nullable でなくします。

type MyNonNullable<Type> = Type extends null | undefined ? never : Type;

#Parameters<Type>

Type という関数型の引数の型をタプル型で返します。

type MyParameters<Type extends (...args: any) => any> = Type extends
  (...args: infer A) => any ? A : never;

#ReturnType<Type>

Type という関数型の戻り値の型を返します。

type MyReturnType<Type extends (...args: any) => any> = Type extends
  (...args: any) => infer R ? R : never;

#ConstructorParameters<Type>

Type 型のコンストラクタの引数の型をタプル型で返します。 抽象クラスも渡せるように、abstract 修飾子をつけています。

type MyConstructorParameters<Type extends abstract new (...args: any) => any> =
  Type extends abstract new (...args: infer Args) => any ? Args : unknown;

#InstanceType<Type>

Type 型のコンストラクタが作るインスタンスの型を返します。

type MyInstanceType<Type extends abstract new (...args: any) => any> =
  Type extends abstract new (...args: any) => infer R ? R : unknown;

#ThisParameterType<Type>

Type という関数型の this の型を返します。

type MyThisParameterType<Type extends (...args: any) => any> = Type extends
  (this: infer T, ...args: any) => any ? T : unknown;

#OmitThisParameter<Type>

Type という関数型から this 引数の型を取り除いた型を返します。

type MyOmitThisParameter<Type extends (...args: any) => any> = Type extends
  (this: any, ...args: infer Args) => infer Ret ? (...args: Args) => Ret : Type;

#ThisType<Type>

ThisType<Type> は、オブジェクトリテラルに生えたメソッド内の this の型を Type にします。

以下は、TypeScript: Documentation - Utility Types より引用した例です。

type ObjectDescriptor<D, M> = {
  data?: D;
  methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {};
  let methods: object = desc.methods || {};
  return { ...data, ...methods } as D & M;
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx; // Strongly typed this
      this.y += dy; // Strongly typed this
    },
  },
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

この例では、methods というオブジェクトがもつ moveBy メソッド内の this に型を付けています。ThisType<D & M> により、 thisD & M という型のオブジェクト、すなわち、

{
    x: number;
    y: number;
} & {
    moveBy: (dx: number, dy: number) => void;
}

という型のオブジェクトになります。

この ThisType<Type> を実装したいのですが、ObjectType & ThisType<Type> というインターフェイスで実装するのは無理だと思われます。 実際、TypeScript から見えるところに ThisType<Type> の実装はなく、空の interface として定義されています。すなわち、実装はコンパイラの内部に隠れています。 代わりに、下記のように使う MyThisType<ObjectType, Type> の実装をすることにします。

type ObjectDescriptor<D, M> = {
  data?: D;
  methods?: MyThisType<M, D & M>; // Type of 'this' in methods is D & M
};
type MyThisType<ObjectType, Type> = {
  [Key in keyof ObjectType]: ObjectType[Key] extends
    (...arg: infer Arg) => infer Ret ? (this: Type, ...arg: Arg) => Ret
    : ObjectType[Key];
};

#Capitalize<StringType>

// "hello, world" -> "Hello, world"

string 型の1文字目と残りの文字列をそれぞれ取り出すには、Template Literal Types と infer を使います

`${infer HeadChar}${infer TailChars}`
type LowerToUpperMap = {
  a: "A";
  b: "B";
  c: "C";
  d: "D";
  e: "E";
  f: "F";
  g: "G";
  h: "H";
  i: "I";
  j: "J";
  k: "K";
  l: "L";
  m: "M";
  n: "N";
  o: "O";
  p: "P";
  q: "Q";
  r: "R";
  s: "S";
  t: "T";
  u: "U";
  v: "V";
  w: "W";
  x: "X";
  y: "Y";
  z: "Z";
};

type HeadCharToUpper<StringType extends string> = StringType extends
  `${infer HeadChar}${infer _}`
  ? (HeadChar extends keyof LowerToUpperMap ? LowerToUpperMap[HeadChar]
    : HeadChar)
  : "";

type MyCapitalize<StringType extends string> = StringType extends
  `${infer HeadChar}${infer TailChars}`
  ? `${HeadCharToUpper<HeadChar>}${TailChars}`
  : "";

#Uncapitalize<StringType>

StringType という文字列リテラル型の先頭の文字が大文字であれば小文字にした型を返します。

// "HELLO WORLD" -> "hELLO WORLD"
type UpperToLowerMap = {
  A: "a";
  B: "b";
  C: "c";
  D: "d";
  E: "e";
  F: "f";
  G: "g";
  H: "h";
  I: "i";
  J: "j";
  K: "k";
  L: "l";
  M: "m";
  N: "n";
  O: "o";
  P: "p";
  Q: "q";
  R: "r";
  S: "s";
  T: "t";
  U: "u";
  V: "v";
  W: "w";
  X: "x";
  Y: "y";
  Z: "z";
};

type HeadCharToLower<StringType extends string> = StringType extends
  `${infer HeadChar}${infer _}`
  ? HeadChar extends keyof UpperToLowerMap ? UpperToLowerMap[HeadChar]
  : HeadChar
  : "";

type MyUncapitalize<StringType extends string> = StringType extends
  `${infer HeadChar}${infer TailChars}`
  ? `${HeadCharToLower<HeadChar>}${TailChars}`
  : "";

#Uppercase<StringType>

StringType という文字列リテラル型の小文字を全て大文字にした型を返します。

// "Hello, world" -> "HELLO, WORLD"

1文字目をさっき作った HeadCharToUpper<StringType> で変換し、残りを MyUppercase に渡して再帰的に変換します。

type LowerToUpperMap = {
  a: "A";
  b: "B";
  c: "C";
  d: "D";
  e: "E";
  f: "F";
  g: "G";
  h: "H";
  i: "I";
  j: "J";
  k: "K";
  l: "L";
  m: "M";
  n: "N";
  o: "O";
  p: "P";
  q: "Q";
  r: "R";
  s: "S";
  t: "T";
  u: "U";
  v: "V";
  w: "W";
  x: "X";
  y: "Y";
  z: "Z";
};

type HeadCharToUpper<StringType extends string> = StringType extends
  `${infer HeadChar}${infer _}`
  ? (HeadChar extends keyof LowerToUpperMap ? LowerToUpperMap[HeadChar]
    : HeadChar)
  : "";

type MyUppercase<StringType extends string> = StringType extends
  `${infer HeadChar}${infer TailChars}`
  ? `${HeadCharToUpper<HeadChar>}${MyUppercase<TailChars>}`
  : "";

長い文字列の型を渡すと、再帰制限にひっかかって下記のエラーになります。

Type instantiation is excessively deep and possibly infinite.

#Lowercase<StringType>

StringType という文字列リテラル型の大文字を全て小文字にした型を返します。

// "Hello, World" -> "hello, world"
type UpperToLowerMap = {
  A: "a";
  B: "b";
  C: "c";
  D: "d";
  E: "e";
  F: "f";
  G: "g";
  H: "h";
  I: "i";
  J: "j";
  K: "k";
  L: "l";
  M: "m";
  N: "n";
  O: "o";
  P: "p";
  Q: "q";
  R: "r";
  S: "s";
  T: "t";
  U: "u";
  V: "v";
  W: "w";
  X: "x";
  Y: "y";
  Z: "z";
};

type HeadCharToLower<StringType extends string> = StringType extends
  `${infer HeadChar}${infer _}`
  ? HeadChar extends keyof UpperToLowerMap ? UpperToLowerMap[HeadChar]
  : HeadChar
  : "";

type MyLowercase<StringType extends string> = StringType extends
  `${infer HeadChar}${infer TailChars}`
  ? `${HeadCharToLower<HeadChar>}${MyLowercase<TailChars>}`
  : "";

#参考

Documentation - Template Literal Types
Documentation - Template Literal Types
Generating mapping types which change properties via template literal strings.
faviconwww.typescriptlang.org
How to UPPER_CASE to camelCase in raw Typescript generics
How to UPPER_CASE to camelCase in raw Typescript generics
TLDR: Today's challenge is to retype an UPPER_CASE static string into camelCase and apply...
favicondev.to

0
2020-Present 9sako6