mosya

mosya<TC> - 文字列を置換するReplaceAll型を定義しよう

この記事はmosya<TC>の問題の一つであるReplaceAll型の解説になります。

問題

文字列Sに含まれる部分文字列FromToに置き換える型ReplaceAll<S, From, To>を実装します。

type replaced = ReplaceAll<
  "t y p e s",
  " ",
  ""
>; // expected to be 'types'

前提知識

この問題を解くにあたって型についての以下の知識を理解しておく必要があります。

  1. Conditional Typesを理解する
  2. inferを理解する
  3. Template Literal Typesを理解する
  4. 再帰的な型を理解する

Conditional Typesを理解する

Conditional Typesは、条件によって型を変更することができる機能です。

例えば、以下のような型が考えられます。

type Foo<T> = T extends string
  ? string
  : number;

この型は、Tstring型を継承している場合はstring型を、そうでない場合はnumber型を返します。

このように、extendsを使って条件を指定することで、型を変更することができます。

inferを理解する

inferは、型を推論することができる機能です。

例えば、以下のような型が考えられます。

type ArrayItem<T> =
  T extends (infer R)[] ? R : never;

この型は、Tが配列の場合は、配列の中の型を返します。

以下のように使うことができます。

type Foo = ArrayItem<string[]>; // string

この場合、infer Rにはstring[]型が当てはまるので、Rstring型に推論され、Rを返すのでstring型が返されます。
このように推論される型を取得するのにinferは役立ちます。

Template Literal Typesを理解する

Template Literal Typesは、文字列リテラルを使って型を作成することができる機能です。型を組み合わせて新しい文字列の型を作成することができます。

例えば、以下のような型が考えられます。

type Foo<T> = `${T} World`;

type HelloWorld = Foo<"Hello">; // "Hello World"

このように、文字列リテラルを使って型を作成することができます。

再帰的な型を理解する

再帰的な型は、自分自身を参照する型のことです。
例えば、以下のような型が考えられます。

type Tree<T> = {
  value: T;
  left: Tree<T>;
  right: Tree<T>;
};

この型は、Treeという型がvalueというプロパティを持ち、leftrightというプロパティはTree型を持つという型です。

以下のような使用例が考えられます。

// 使用例
const node: Tree = {
  value: 1,
  left: {
    value: 2,
    left: null,
    right: null,
  },
  right: {
    value: 3,
    left: null,
    right: {
      value: 4,
      left: null,
      right: null,
    },
  },
};

このように、再帰的な型は繰り返し同じ型を再利用したい場合に便利です。

解答例

type ReplaceAll<
  S extends string,
  From extends string,
  To extends string
> = From extends ""
  ? S
  : S extends `${infer L}${From}${infer R}`
  ? `${L}${To}${ReplaceAll<
      R,
      From,
      To
    >}`
  : S;

まず、引数の、S, From, Toにはそれぞれ文字列型が入るので、S extends string, From extends string, To extends stringというように制約をつけます。

type ReplaceAll<S extends string, From extends string, To extends string> = ...

次に、Fromが空文字の場合は、Sをそのまま返すようにします。

type ReplaceAll<S extends string, From extends string, To extends string> = 
    From extends '' ? S : ...

Fromが空文字でない場合は、SがFromを含む場合と含まない場合に分けます。

type ReplaceAll<S extends string, From extends string, To extends string> = 
    From extends '' ? S : S extends `${infer L}${From}${infer R}` ? ... : ...

SがFromを含む場合は、Fromの前後にある文字列を取得して、Toを挟みます。

type ReplaceAll<S extends string, From extends string, To extends string> = 
    From extends '' ? S : S extends `${infer L}${From}${infer R}` ? `${L}${To}${R}` : ...

SがFromを含まない場合は、Sをそのまま返します。

type ReplaceAll<
  S extends string,
  From extends string,
  To extends string
> = From extends ""
  ? S
  : S extends `${infer L}${From}${infer R}`
  ? `${L}${To}${R}`
  : S;

さらに、${L}${To}${R}の部分に関しては、${R}の部分にFromが含まれる可能性があるので、再帰的にReplaceAllを呼び出します。

type ReplaceAll<
  S extends string,
  From extends string,
  To extends string
> = From extends ""
  ? S
  : S extends `${infer L}${From}${infer R}`
  ? `${L}${To}${ReplaceAll<
      R,
      From,
      To
    >}`
  : S;

これで、FromをToに置き換えることができました。

Authored by

筆者の写真

Godai@steelydylan

Webサービスを作るのが好きなWebエンジニア。子供が産まれたことをきっかけに独立し法人化。サービス開発が大好き。
好きな言語はTypeScript。

ReactやTypeScriptなどの周辺技術が学べる
オンライン学習サービスを作りました!

詳しくはこちら
mosya

mosyaはオンラインでHTML,CSS,JavaScriptを基本から学習できるサービスです。現役エンジニアが作成した豊富なカリキュラムに沿って学習を進めましょう。

© 2023 - mosya. All rights reserved.