🐢

Intersection型(A & B)で合成された配列はアクセス方法によって取り出した要素の型が変わる

2024/10/04に公開

const a, const b の型を想像してみて欲しい

type C0 = { employees: E1[] };
type CX = C0 & { employees: E2[] };

function test1(company: CX) {
    const a = company.employees[0];
    const b = company.employees.map((x) => x)[0];
}
  • a の型は E1 & E2 // それはそう
  • b の型は E1 // なんでェ.....

🤔🤔🤔 嘘だろ... なんで... なんでなんだ... ただの配列アクセスだろ... 🤨🤨🤨

これはこういう仕様?

公式ドキュメントっぽい情報がみつけられないが、Microsoft でTypescriptの開発のリードをやっているRyanCavanaughさんのコメントによるとこれはこういうものらしい
https://github.com/Microsoft/TypeScript/issues/11961#issuecomment-257444466

このような型合成はさけるべきだがうっかりに注意したい(自戒)

RyanCavanaughのコメントにもあるが
そもそも{ list: E1[] } & { list: E2[] } みたいなコードにならない方がよい.

{ list: E1[] } & { list: E2[] } という字面をみたら、あーなんかこれは型の解釈はどうなるんだろうか、不安だからやめておこうという気持ちになるかもしれない。
自分の主観だが、既存の型になにか付け加えた結果 { list: E1[] } & { list: E2[] } をうっかり作ってしまうことが多い気がする

// こういう型が先に存在するとして
interface Employee = { }
interface Company {
  ... たくさんのproperties
  employees: Employee[]
}

// 「
//    employees  部分だけちょっと型の違うemployeeにしたいんだよね
//    一部違うだけだからほとんど同じたくさんのpropertiesを再度書いて欲しい型を作りたくない
// 」
// という気持ちになり...

type CustomEmployee = { ... }
type CompanyWithX = Company & {
  employees: CustomEmployee[]
}
// ↑ ほら配列要素アクセス時にトラップのある配列のできあがり

対処方法

意図しない方の型をちゃんとOmitしておけばよい。

- type CompanyWithX = Company & {
+ type CompanyWithX = Omit<Company, "employees">
    employees: CustomEmployee[]
  }

Discussion