You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript组合扩展类型时属性依赖必选校验失效问题

问题描述

以下是一个单独使用时逻辑正常,但组合为更大类型时会出现异常的TypeScript类型场景:

核心约束规则:

  • 若定义了scrollToItem,则必须同时定义getRowId
  • 若未定义scrollToItem,则getRowId为可选属性

最初的类型定义可以正常工作:

type ScrollProps<T> =
  | { scrollToItem: T; getRowId: (row: T) => string; }
  | { getRowId?: (row: T) => string; };

const a: ScrollProps<number> = {}; // ok
const b: ScrollProps<number> = { scrollToItem: 6, getRowId: (row) => '' }; // ok
const c: ScrollProps<number> = { getRowId: (row) => '' }; // ok
const d: ScrollProps<number> = { scrollToItem: 6 }; // error - 符合预期

后续扩展为从导入的库类型中选取getRowId属性时,逻辑依然正常:

type LibraryObject<T> = {
  getRowId?: (row: T) => string;
  otherProps?: boolean;
};

type ScrollProps<T> =
  | ({ scrollToItem: T } & Required<Pick<LibraryObject<T>, 'getRowId'>>)
  | Pick<LibraryObject<T>, 'getRowId'>;

const a: ScrollProps<number> = {}; // ok
const b: ScrollProps<number> = { scrollToItem: 6, getRowId: (row) => '' }; // ok
const c: ScrollProps<number> = { getRowId: (row) => '' }; // ok
const d: ScrollProps<number> = { scrollToItem: 6 }; // error - 符合预期

但将该类型与已继承库类型的现有类型组合时,TypeScript校验逻辑出现异常:当传入scrollToItem的同时也传入了被扩展类型的其他属性时,TypeScript不再强制要求传入getRowId。复现代码如下:

type LibraryObject<T> = {
  getRowId?: (row: T) => string;
  otherProps?: boolean;
};

type ScrollProps<T> =
  | ({ scrollToItem: T } & Required<Pick<LibraryObject<T>, 'getRowId'>>)
  | Pick<LibraryObject<T>, 'getRowId'>;

type MasterType<T> = LibraryObject<T> & ScrollProps<T>;

const a: MasterType<number> = {}; // ok
const b: MasterType<number> = { scrollToItem: 6, getRowId: (row) => '' }; // ok
const c: MasterType<number> = { getRowId: (row) => '' }; // ok
const d: MasterType<number> = { scrollToItem: 6 }; // error - 符合预期
const e: MasterType<number> = { scrollToItem: 6, otherProps: true }; // 无报错,不符合预期

问题原因

这个问题的本质是两个TypeScript类型系统特性共同作用的结果:

  • 对象类型默认是开放的:TS的对象类型不会禁止类型中未声明的属性,只要所有必填属性满足要求即可。我们常说的"多余属性报错"只是针对对象字面量直接赋值的特殊检查(excess property checking),不是类型系统的核心规则。
  • 联合类型匹配不会自动推断互斥属性:定义的ScrollProps第二个分支,仅声明了getRowId为可选,没有显式禁止scrollToItem属性存在。

当ScrollProps单独使用时,第二个分支的类型只有getRowId?: (row:T)=>string,传入scrollToItem时会触发多余属性检查,因此能正确报错。但当交叉LibraryObject<T>生成MasterType后,联合类型的第二个分支会被展开为{ getRowId?: (row:T)=>string; otherProps?: boolean },此时传入otherProps: true会让TS判定对象匹配第二个分支,scrollToItem:6不会触发多余属性拦截,最终绕过了getRowId的必填校验。


修复方案

给联合类型的第二个分支显式添加互斥属性声明,将scrollToItem标记为never类型,明确告知TS该分支下不允许存在scrollToItem属性:

type ScrollProps<T> =
  | ({ scrollToItem: T } & Required<Pick<LibraryObject<T>, 'getRowId'>>)
  | Pick<LibraryObject<T>, 'getRowId'> & { scrollToItem?: never };

type MasterType<T> = LibraryObject<T> & ScrollProps<T>;

// 校验逻辑恢复正常
const e: MasterType<number> = { scrollToItem: 6, otherProps: true }; 
// 报错:类型 "{ scrollToItem: number; otherProps: boolean; }" 中缺少属性 "getRowId"

修复后无论是否传入otherProps等其他库属性,只要存在scrollToItem,TS就会强制要求传入getRowId,完全符合预期约束。


内容的提问来源于stack exchange,提问作者HaveSpacesuit

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 10:48:43