【TypeScript】Optional Propertyと、任意の型とundefinedのUnion型にするのでは挙動が違う話
こんにちは!はじめまして。昨年の9月に入社したカネミツです。
もともとバックエンドメインでフロントエンドは兼業みたいな感じで今までやっていたのですが、
Nextatに入社してからはフロントエンドメインにやらせて頂いてます。
TypeScriptやNext.jsを普段メインに触っています。
あまり慣れていない技術にもチャレンジできて毎日楽しいです。
概要
先に結論を言うと、タイトルの通りなのですが、
foo?: string
foo: string | undefined
の2つは挙動が違うよという話になります。完全に私が仕様を勘違いしていました…。
気になった理由
今担当している業務ではNext.jsを使用しているので、私は以下のようなコードを書いていました。
interface FooState {
foo?: string;
bar?: string;
}
// 省略
const [state, setState] = useState<FooState>({});
FooState
という型を宣言してstateに使用しています。初期値に空のオブジェクトをセットしています。
その後、他の方の作成したコードを修正する機会があったのですが、そこでは
interface FooState {
foo: string | undefined;
bar: string | undefined;
}
のように書かれていたので、それに合わせて以下のように新しくStateを追加しました。
interface HogeState {
hoge: string | undefined;
fuga: number | undefined;
}
// 省略
const [state, setState] = useState<HogeState>({});
すると、
TS2345: Argument of type '{}' is not assignable to parameter of type 'HogeState | (() => HogeState)'.
とエラーが出たので、え?もしかしてOptional Propertyにするのと、string | undefined
みたいにUnion型にするのって挙動が違うの?ってなったのでちょっと気になったので調べることにしました。
環境
- TypeScript 4.9.5
- Node.js 16.13.1
何が違うのか?
実際に以下のような適当なコードを書いて実験してみます。
interface Point {
x: number;
y?: number;
}
const point1: Point = { x: 1, y: 1 };
const point2: Point = { x: 2 };
const point3: Point = { x: 3, y: undefined };
console.log(point1);
console.log(point2);
console.log(point3);
Pointを定義して、Point型の変数point1, 2, 3を宣言しました。 それぞれ、
- point1:全部のプロパティを設定
- point2:xだけ設定
- point3:xには数値を設定し、yにはundefinedを設定
コンソールに出力した結果は
{ x: 1, y: 1 }
{ x: 2 }
{ x: 3, y: undefined }
のようになります。 上の実験結果と気になった理由の項目で記述しているエラー内容から、次のようなことがわかります。
-
Optional Propertyの場合 そのオブジェクトの中に、プロパティが存在してもしなくてもいい。 プロパティが存在した場合は、設定される値が指定した型かundefinedなら良いという意味 (後述する
exactOptionalPropertyTypes
というコンパイラオプションをtrueにした場合、undefined
の代入を禁止します) -
任意の型と
undefined
で型指定をした場合 そのオブジェクトの中にプロパティが存在しなければいけない。ただし、設定される値がundefined
でも良いという意味
関連調査
TypeScriptの公式ドキュメントをOptional Propertyで検索していてたら、exactOptionalPropertyTypesというOptional Propertyに関するコンパイラオプションが存在することを知りました(参考欄のexactOptionalPropertyTypesについてにリンクを貼ってます)
これを有効にすると、型宣言より前に?を持つ型やインターフェースのプロパティの扱いについてより厳しい規則を適用するよ、と書いてあります。
// @exactOptionalPropertyTypes
// @errors: 2322 2412
interface UserDefaults {
colorThemeOverride?: "dark" | "light";
}
declare function getUserSettings(): UserDefaults;
// ---cut---
const settings = getUserSettings();
settings.colorThemeOverride = "dark";
settings.colorThemeOverride = "light";
// But not:
settings.colorThemeOverride = undefined;
上記は公式ドキュメントにあるコードを引用したものです、//But not:の次の行で
Type 'undefined' is not assignable to type '"dark" | "light"' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the type of the target.(2412)
というエラーが出ます。 いま、colorThemeOverride
はプロパティが存在すれば、"dark"
または"light"
の値をとるように定義されているのでundefined
を設定することはできません。エラーメッセージにもこう書きたいならundefinedを型に追加するようにと出ていますね!
まとめ
どっちを使えばいいかな?と考えてみましたが、個人的には任意の型とundefined
のUnion型の方が良いかもなと思いました。たとえば、自分がよく使うObject.keys()
やObject.entries()
なんかを使用するときに、プロパティが存在するオブジェクトと、存在しないオブジェクトが混ざると処理も増えて面倒だなと感じました。
参考
- Optional Propertyについて
- exactOptionalPropertyTypesについて