スプレッド構文とは
...
を使用して配列の要素を展開出来るもんです
多くの方が使用しているかと思われます
何に注意が必要なのか
下記コードを見てエラーになるだろ、感じる方がいるかと思われます
ですが予想に反してエラーにならず実行が出来てしまいます
const userLog = (arg: { name: string; email: string }) => {
console.log(arg);
};
const userName = {
name: "Alice",
};
const userEmail = {
email: "alice@example.com",
};
const userAge = {
age: 30,
};
userLog({ ...userName, ...userEmail, ...userAge });
// { name: 'Alice', email: 'alice@example.com', age: 30 }
なら型注釈をつけてみたらどうでしょう?
残念ながらエラーにならず実行が出来てしまいます
type Arg = { name: string; email: string };
const userLog = (arg: Arg) => {
console.log(arg);
};
const userName = {
name: "Alice",
};
const userEmail = {
email: "alice@example.com",
};
const userAge = {
age: 30,
};
const user: Arg = { ...userName, ...userEmail, ...userAge };
userLog(user);
// { name: 'Alice', email: 'alice@example.com', age: 30 }
スプレッド構文を使わないでコード書いてみるとどうでしょう
この場合はしっかりエラーになってくれます
const userLog = (arg: { name: string; email: string }) => {
console.log(arg);
};
const userName = {
name: "Alice",
};
const userEmail = {
email: "alice@example.com",
};
const userAge = {
age: 30,
};
userLog({
name: "Alice",
email: "alice@example.com",
age: 30, // Object literal may only specify known properties, and 'age' does not exist in type '{ name: string; email: string; }'.
});
なぜこうなるのか
理由としては構造的部分型
と余剰プロパティチェック
によって上記の現象が引き起こされています
構造的部分型とは
今回でいうと型{ name: string; email: string }
を要求された時にそれ以上のプロパティを持っていても互換性があると見做されるルールのことです
余剰プロパティチェックとは
オブジェクトリテラルを直接代入する際に定義されていないプロパティが混じっていないか追加で検査する仕組みです
ここで疑問に思うのは
userLog({ ...userName, ...userEmail, ...userAge });
は直接渡しているけどエラーにならないのか?
理由としてオブジェクトリテラルを直接
ではなく計算結果のオブジェクト
のため余剰プロパティチェックが働かないためです
対策として
どう対策としてどうすればよいでしょう?
テストをしっかり行ったりzod
を使用して対策するのも良いでしょう(というよりzod
を使用するほうが現実的かと)
ですが今回敢えて素のTypeScriptで対策する手段を紹介します
それが下記のコードになります
type Arg = { name: string; email: string };
type userLog = <T extends Arg>(
arg: T & Record<Exclude<keyof T, keyof Arg>, never>
) => void;
流れとして
1. T extends Arg
少なくともArg = { name: string; email: string }
を持つ型であることを保証します
2. ReCord<Exclude<keyof T, keyof Arg>, never>
T
のキーのうちArg = { name: string; email: string }
に含まれないものはnever
に制約します
これによりArg = { name: string; email: string }
に無いプロパティを持っていた場合はエラーになります
3. T & Record<Exclude<keyof T, keyof Arg>, never>
余分なキーがあるとnever
との交差で整合性が取れなくなり型エラーになります
全体像
type Arg = { name: string; email: string };
type userLog = <T extends Arg>(
arg: T & Record<Exclude<keyof T, keyof Arg>, never>
) => void;
const userLog: userLog = (arg) => {
console.log(arg);
};
const userName = {
name: "Alice",
};
const userEmail = {
email: "alice@example.com",
};
const userAge = {
age: 30,
};
const user = {
...userName,
...userEmail,
...userAge,
};
userLog(user); // Argument of type '{ age: number; email: string; name: string; }' is not assignable to parameter of type '{ age: number; email: string; name: string; } & Record<"age", never>'. Type '{ age: number; email: string; name: string; }' is not assignable to type 'Record<"age", never>'. Types of property 'age' are incompatible. Type 'number' is not assignable to type 'never'.
まとめ
今回は実際に実務で遭遇、対策を講じたものを記事用に書いてみました
TypeScriptのルールや機能について色々調べて新たな発見をすることが出来ました