mosya
mosya Business はこちら

mosya<TC> - オブジェクト型の一部を選択する型を作ろうの解説

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

問題

例えば以下のようなコードを満たすようにMyPick型を実装しましょう。

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = MyPick<
  Todo,
  "title" | "completed"
>;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

前提知識

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

  1. ジェネリクスを理解する
  2. Conditional Typesを理解する
  3. never型を理解する

ジェネリクスを理解する

まず、<T>というのは、ジェネリクスと呼ばれる機能です。ジェネリクスは、型をパラメータとして受け取ることができる機能です。

この受け取った値を使って、新しい型を生成することができます。
受け取った型を活用して、新しい型を生成することができるので、型の再利用性が高くなります。

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

type Foo<T> = {
  bar: T;
};

この型は、Tという型を受け取り、barというプロパティにTを代入する型です。

この型を使うと、以下のように型を指定することができます。

type FooString = Foo<string>; // { bar: string }
type FooNumber = Foo<number>; // { bar: number }

このようにジェネリクスは型を引数として受け取って、新しい型を生成することができます。

型の制約

Textendsをつけることで制約をつけることができます。
例えば、T extends stringとすると、Tstring型かstring型を継承した型に制約されます。

Conditional Typesを理解する

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

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

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

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

type FooString = Foo<string>; // string
type FooNumber = Foo<boolean>; // number

never型を理解する

never型は、発生しない値の型です。
この型を使うと、存在しない型をないものとして扱うことができます。

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

type Exclude<T, U> = T extends U
  ? never
  : T;

この型は、TUを継承している場合はnever型、そうでない場合はT型になります。
以下のような使い方ができます。

type Foo = Exclude<
  string | number | boolean,
  boolean
>; // string | number

string | number | booleanbooleanを継承しているので、式に当てはめるとnever型になります。
よって、最終的にFoostring | numberになります。

また、Tからundefinednullを除外する型もnever型を使って実装できます。

type NonNullable<T> = T extends
  | null
  | undefined
  ? never
  : T;

このように、存在しないことが保証される型をnever型を使って表現することができます。

まとめ

以上の前提知識を踏まえて、この問題を解いていきましょう。

まず、MyPick型を実装するにあたって、以下のような型を考えることができます。

type MyPick<T, K> = {
  [P in K]: T[P];
};

ただし、この状態だと型エラーになります。というのもKTのプロパティであることが保証されていないからです。

そこで、KTのプロパティであることを保証するために、以下のようにextendsを使って制約をつけます。

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

これで、KTのプロパティであることが保証されました。

さらに実装を改善する

さらに、KにはTのプロパティ以外のユニオンが含まれてもいいように実装してみましょう。
キーにはstringnumbersymbolなどのプリミティブ型が指定でき、Tのプロパティ以外のユニオンが含まれている可能性があるものとします。

type MyPick<
  T,
  K extends
    | keyof T
    | string
    | number
    | symbol
> = {
  [P in K]: T[P];
};

この場合、T[P]の部分でエラーになります。というのも、TにはPというプロパティが存在しない可能性があるからです。

そこで、T[P]の部分を以下のように書き換えます。

type MyPick<
  T,
  K extends
    | keyof T
    | string
    | number
    | symbol
> = {
  [P in K]: P extends keyof T
    ? T[P]
    : never;
};

これで、TにはPというプロパティが存在しない場合はnever型になるので、確実にTのプロパティである場合のみオブジェクトに含まれるようになりました。

Authored by

筆者の写真

Godai@steelydylan

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

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

詳しくはこちら
mosya

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

© 2023 - mosya. All rights reserved.