2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptでスプレッド構文の利用にはご注意を

Posted at

スプレッド構文とは

...を使用して配列の要素を展開出来るもんです
多くの方が使用しているかと思われます

何に注意が必要なのか

下記コードを見てエラーになるだろ、感じる方がいるかと思われます
ですが予想に反してエラーにならず実行が出来てしまいます

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; }'.
});

CleanShot 2025-09-29 at 22.23.01.png

なぜこうなるのか

理由としては構造的部分型余剰プロパティチェックによって上記の現象が引き起こされています

構造的部分型とは

今回でいうと型{ 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'.

CleanShot 2025-09-29 at 22.28.38.png

まとめ

今回は実際に実務で遭遇、対策を講じたものを記事用に書いてみました
TypeScriptのルールや機能について色々調べて新たな発見をすることが出来ました

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?