【TypeScript】 オブジェクトのnullableなプロパティをnon-nullに変換する【型定義】
はじめに
TypeScriptでオブジェクトの一部(または全部)のプロパティをnullableで型定義し、そのオブジェクトのnull許容させない型を作るときにどうすれば良いのかメモします。
↓のような定義
type User = {
id: number,
name: string | null,
age: number | null,
}
すべてのプロパティをnon-nullにする方法
TL;DR
type User = {
id: number,
name: string | null,
age: number | null,
}
type RequiredNotNull<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
type NonNullableUser = RequiredNotNull<User>
// ↓のような定義になります
// NonNullableUser = {
// id: number,
// name: string,
// age: number,
// }
解説
type NonNullableUser = RequiredNotNull<User>
ここでRequiredNotNull
型のジェネリクスにUser型を渡しています。
type RequiredNotNull<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
<T>にはUser型が入ってきます。
[P in keyof T]
はいわゆるMapped Typesとよばれる型定義の手法でin演算子とkeyof演算子を合わせた組み合わせたものになります。
keyof演算子とは
keyof演算子はオブジェクトのプロパティ名のみを型として返してくれる演算子です。
type UserKey = keyof T // "id" | "name" | "age"
参考
つまり[P in keyof T]
とは[P in "id" | "name" | "age"]
と同義です。
in演算子とは
Javascriptにもin演算子がありますが、今回はTypeScript独自の使い方です。
これは<T>のプロパティをもとに新しいプロパティを生成してくれます。
プロパティを全てmapしてくれるようなやり方でMapped Typeと呼ばれています。
type RequiredNotNull<User> = {
[P in keyof User]: string;
};
// ↓のような型定義になります。
// type RequiredNotNull<User> = {
// id: string;
// name: string;
// age: string;
// };
参考
T[P] is 何?
type RequiredNotNull<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
の部分のT[P]
って何やってるん?ってなりそうですが、これはプロパティ個々の型を取得しています。
type Id = User["id"] // number
ということですね。
NonNullable is 何?
Utility typesです。
Utility typesとは型変換を容易にするためにTypeScriptが提供している機能となります。
下記の記事にあるように、たくさんあるのでここでは説明しませんが今回はUtility typesのなかのNonNullableという、型からnullとundefinedを取り除く型を作成する機能を作ります。
type Name = string | null;
type NonNullableName = NonNullable<Name> // string
以上を踏まえてまとめると、、、
最初に定義した型をもう一度見てみると、さまざまなTypeScriptの機能を駆使してnon-nullな型定義を行なっていることがわかります。
- key of演算子でUser型をプロパティ名のみの型として取得
- in演算子でその型を一つずつmapして新たなプロパティを作成
- NonNullableでnullを取り除いた型を作成
type User = {
id: number,
name: string | null,
age: number | null,
}
type RequiredNotNull<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
type NonNullableUser = RequiredNotNull<User>
// ↓のような定義になります
// NonNullableUser = {
// id: number,
// name: string,
// age: number,
// }
一部のプロパティをnon-nullにする方法
先ほどはname
もage
もnon-nullにしましたが、age
のみnon-nullにしたい場合はどのようにすれば良いでしょうか?🤔
TL;DR
新たにPickPropsという型を挟んで実装してみました。
type User = {
id: number,
name: string | null,
age: number | null,
}
type RequiredNotNull<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
// 新たに追加
type PickProps<T, K extends keyof T> = T & RequiredNotNull<Pick<T, K>>;
type NonNullableUser = PickProps<User, "age">;
// ↓のような定義になります
// NonNullableUser = {
// id: number,
// name: string | null,
// age: number,
// }
解説
extendsとは
extendsを用いることで、型を限定させることができます。これだけではなんのこっちゃと思うので一つずつ説明します。
ジェネリクスTにはUser
、Kにはage
が入ります。PickProps<User, "age" extends keyof User>
ということですね。
先ほど説明したようにkeyof演算子でkeyのみの型を作成できるので、
"age" extends "id" | "name" | "age"
と同義です。
つまり、どういうことかというとKにはTに含まれたプロパティ名しかジェネリクスとして渡すことはできないよ という縛りをつけることができます。
ですので、ここでUserにはないプロパティ名を渡すとTypeScript側でコンパイルエラーを出してくれます。
type NonNullableUser = PickProps<User, "pref">;
// エラー
Pick<T, K>とは
NonNullable同様Utility typesです。
T型の中からKで選択したプロパティのみの型を作成することができます。
Pick<User, "age">
なので、ageのみの型を作成するという意味合いですね。
type PickedUser = Pick<User, "age">
// PickedUser = { age: number | null };
そんでもってNonNullable
を使うことでageのnullを排除することができますね。
type RequiredNotNull<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
type PickedUser = RequiredNotNull<Pick<T, K>>;
// PickedUser = { age: number };
& is 何?
T & RequiredNotNull<Pick<T, K>>;
のTの後ろの&
は何をしているかというと、プロパティをインターセクションしています。AかつBということで、プロパティを付け加えたい時などに使われます。
type User = {
id: number;
name: string | null;
age: number | null;
}
type AddedPrefUser = User & { pref: string }
// {
// id: number;
// name: string | null;
// age: number | null;
// pref: string
// }
ここで注意したいのがプロパティ名が被った場合はどのような挙動になるのかということです。
今回で言うと、User
と{ age: number }
をくっつけたいと言うことになりますが、もちろんUser
にもage
がいます。age
というプロパティが既に存在しているのでエラーが発生するのか?とも思いますが、そうはなりません。
ここがAかつBといった意味なんですが、名前が競合した場合は両方にある型のみを抽出します。
今回{ age: number | null }
と{ age: number }
が競合するので、両方の型に存在するnumber
のみが残ります。
type User = {
id: number;
name: string | null;
age: number | null;
}
type AddedUser = User & { age: number }
// {
// id: number;
// name: string | null;
// age: number; // nullがなくなった!
// }
以上を踏まえて再度説明すると、、、
- extendsキーワードで型限定をさせつつ、
- Pickでageプロパティのみの型を作成
- &を用いて型を結合
- key of演算子でUser型をプロパティ名のみの型として取得
- in演算子でその型を一つずつmapして新たなプロパティを作成
- NonNullableでnullを取り除いた型を作成
みたいな流れですかね。数行で型定義のために色々やっていることがわかります。
type User = {
id: number,
name: string | null,
age: number | null,
}
type RequiredNotNull<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
// 新たに追加
type PickProps<T, K extends keyof T> = T & RequiredNotNull<Pick<T, K>>;
type NonNullableUser = PickProps<User, "age">;
// ↓のような定義になります
// NonNullableUser = {
// id: number,
// name: string | null,
// age: number,
// }
まとめ
TypeScriptの型定義は最初の方は難しく感じていましたが、最近は少しずつ楽しくなってきました。
まだまだTSビギナーですので、間違えていたらご指摘いただける嬉しいです!😎
Discussion