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,
};
前提知識
この問題を解くにあたって型についての以下の知識を理解しておく必要があります。
- ジェネリクスを理解する
- Conditional Typesを理解する
- never型を理解する
ジェネリクスを理解する
まず、<T>というのは、ジェネリクスと呼ばれる機能です。ジェネリクスは、型をパラメータとして受け取ることができる機能です。
この受け取った値を使って、新しい型を生成することができます。
受け取った型を活用して、新しい型を生成することができるので、型の再利用性が高くなります。
例えば、以下のような型が考えられます
type Foo<T> = {
bar: T;
};
この型は、T
という型を受け取り、bar
というプロパティにT
を代入する型です。
この型を使うと、以下のように型を指定することができます。
type FooString = Foo<string>; // { bar: string }
type FooNumber = Foo<number>; // { bar: number }
このようにジェネリクスは型を引数として受け取って、新しい型を生成することができます。
型の制約
T
はextends
をつけることで制約をつけることができます。
例えば、T extends string
とすると、T
はstring
型かstring
型を継承した型に制約されます。
Conditional Typesを理解する
Conditional Typesは、条件によって型を変更することができる機能です。
例えば、以下のような型が考えられます。
type Foo<T> = T extends string
? string
: number;
この型は、T
がstring
型を継承している場合はstring
型、そうでない場合はnumber
型になります。
type FooString = Foo<string>; // string
type FooNumber = Foo<boolean>; // number
never型を理解する
never
型は、発生しない値の型です。
この型を使うと、存在しない型をないものとして扱うことができます。
例えば、以下のような型が考えられます。
type Exclude<T, U> = T extends U
? never
: T;
この型は、T
がU
を継承している場合はnever
型、そうでない場合はT
型になります。
以下のような使い方ができます。
type Foo = Exclude<
string | number | boolean,
boolean
>; // string | number
string | number | boolean
はboolean
を継承しているので、式に当てはめるとnever
型になります。
よって、最終的にFoo
はstring | number
になります。
また、T
からundefined
とnull
を除外する型もnever
型を使って実装できます。
type NonNullable<T> = T extends
| null
| undefined
? never
: T;
このように、存在しないことが保証される型をnever
型を使って表現することができます。
まとめ
以上の前提知識を踏まえて、この問題を解いていきましょう。
まず、MyPick
型を実装するにあたって、以下のような型を考えることができます。
type MyPick<T, K> = {
[P in K]: T[P];
};
ただし、この状態だと型エラーになります。というのもK
がT
のプロパティであることが保証されていないからです。
そこで、K
がT
のプロパティであることを保証するために、以下のようにextends
を使って制約をつけます。
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
これで、K
がT
のプロパティであることが保証されました。
さらに実装を改善する
さらに、K
にはT
のプロパティ以外のユニオンが含まれてもいいように実装してみましょう。
キーにはstring
やnumber
、symbol
などのプリミティブ型が指定でき、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。