🐥

【TypeScript】 オブジェクトのnullableなプロパティをnon-nullに変換する【型定義】

2022/07/17に公開

はじめに

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"

参考
https://typescriptbook.jp/reference/type-reuse/keyof-type-operator

つまり[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;
// };

参考
https://github.com/Microsoft/TypeScript/pull/12114

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を取り除く型を作成する機能を作ります。
https://qiita.com/k-penguin-sato/items/e2791d7a57e96f6144e5

type Name = string | null;

type NonNullableName = NonNullable<Name> // string

以上を踏まえてまとめると、、、

最初に定義した型をもう一度見てみると、さまざまなTypeScriptの機能を駆使してnon-nullな型定義を行なっていることがわかります。

  1. key of演算子でUser型をプロパティ名のみの型として取得
  2. in演算子でその型を一つずつmapして新たなプロパティを作成
  3. 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にする方法

先ほどはnameageも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がなくなった!
// }

以上を踏まえて再度説明すると、、、

  1. extendsキーワードで型限定をさせつつ、
  2. Pickでageプロパティのみの型を作成
  3. &を用いて型を結合
  4. key of演算子でUser型をプロパティ名のみの型として取得
  5. in演算子でその型を一つずつmapして新たなプロパティを作成
  6. 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ビギナーですので、間違えていたらご指摘いただける嬉しいです!😎

GitHubで編集を提案

Discussion