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

TypeScript如何区分属性缺失与赋值undefined?限制函数仅接收单属性对象

TypeScript中区分属性缺失与显式赋值undefined的方案

你当前的写法之所以无法区分,是因为b?: never等价于b: never | undefined——TypeScript会把可选属性的类型默认加上undefined,所以显式传入b: undefined会被判定为符合类型约束。要实现你想要的校验,需要让类型严格禁止存在未预期的属性(哪怕是赋值为undefined的属性)。

下面给出两种可行的实现方案:

方案一:自定义严格联合类型

通过辅助类型让联合类型的每个成员都严格排斥其他成员的属性:

// 辅助类型:将联合类型转换为交叉类型
type UnionToIntersection<U> = 
  (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

// 辅助类型:生成严格的联合类型,每个成员不能包含其他成员的属性
type StrictUnion<T> = 
  T extends unknown ? T & Record<Exclude<keyof UnionToIntersection<T>, keyof T>, never> : never;

// 定义基础接口
interface A {
  a: number;
}

interface B {
  b: number;
}

// 生成严格的参数类型
type StrictArg = StrictUnion<A | B>;

const f = (arg: StrictArg) => {};

// ✅ 合法调用
f({ a: 0 });

// ❌ 报错:Type 'number' is not assignable to type 'never'
f({ a: 0, b: 0 });

// ❌ 报错:Type 'undefined' is not assignable to type 'never'
f({ a: 0, b: undefined });

这个方案的核心是StrictUnion类型:它先把联合类型转成交叉类型,获取所有可能的属性名,然后给每个联合成员加上约束——不属于该成员的其他属性必须是never类型,这样不管是传有效值还是undefined,只要属性存在就会触发类型错误。

方案二:使用satisfies运算符做局部校验

如果不想全局定义严格类型,可以用TypeScript 4.9+新增的satisfies运算符,在函数内部或参数层面做校验:

interface A {
  a: number;
}

interface B {
  b: number;
}

type Arg = A | B;

const f = (arg: Arg) => {
  // 强制校验参数不能包含额外属性
  const _ = arg satisfies Arg & Record<string, never>;
};

// ✅ 合法调用
f({ a: 0 });

// ❌ 报错:类型不满足约束条件
f({ a: 0, b: 0 });

// ❌ 报错:类型不满足约束条件
f({ a: 0, b: undefined });

或者直接把校验逻辑写到参数类型里:

const f = <T extends Arg>(
  arg: T extends A 
    ? Exclude<keyof T, keyof A> extends never ? T : never 
    : T extends B 
      ? Exclude<keyof T, keyof B> extends never ? T : never 
      : never
) => {};

应用到React useReducer场景

把这个逻辑迁移到useReducer的action类型定义上,就能避免误传属性导致state被设为undefined:

type StrictUnion<T> = 
  T extends unknown ? T & Record<Exclude<keyof UnionToIntersection<T>, keyof T>, never> : never;
type UnionToIntersection<U> = 
  (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type State = { a: number; b: number };

type Action = StrictUnion<
  { type: 'UPDATE_A'; payload: { a: number } } |
  { type: 'UPDATE_B'; payload: { b: number } }
>;

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'UPDATE_A':
      return { ...state, ...action.payload }; // 仅更新a,保留原有b的值
    case 'UPDATE_B':
      return { ...state, ...action.payload }; // 仅更新b,保留原有a的值
    default:
      return state;
  }
};

// ✅ 合法调度
dispatch({ type: 'UPDATE_A', payload: { a: 10 } });

// ❌ 报错:不允许传入额外的b属性
dispatch({ type: 'UPDATE_A', payload: { a: 10, b: undefined } });

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 00:15:42