使用非共享属性 (TypeScript) 时,从联合类型的对象中省略共享 属性 会导致错误

Omitting a shared property from a union type of objects results in error when using non-shared properties (TypeScript)

我正在创建一个包装函数,它将 refs 属性 传递给 send 函数,如下所示。用于创建我的状态机的事件类型定义为基本接口 { refs: NodeRefs } 和可能事件对象的联合之间的交集,例如{ type: "EVENT_1" } | { type: "EVENT_2", context: MyContextType }.

包装函数(示例代码中的useMachine)应该return我们更新的send函数,它需要一个省略refs键的事件对象。然后在此处使用 Omit 尝试将我的发送函数与来自联合类型的任何非共享属性一起使用时会导致错误,我不是 100% 清楚为什么或如何以不同的方式执行此操作。

enum States {
  Unchecked = "UNCHECKED",
  Checked = "CHECKED",
  Mixed = "MIXED"
}

enum Events {
  Toggle = "TOGGLE",
  Set = "SET",
  UpdateContext = "UPDATE_CONTEXT"
}

// Events for the state machine will be a union type a la TEvent, but all events
// will need a `refs` property. We just won't need to explicitly pass refs in
// our event calls thanks to the useMachine hook below
interface EventBase {
  refs: NodeRefs;
}

type TEvent = EventBase &
  (
    | { type: Events.Toggle }
    | {
        type: Events.Set;
        state: States;
      }
    | {
        type: Events.UpdateContext;
        context: Partial<Context>;
      }
  );

function useMachine(stateMachine: SomeStateMachine, refs: ReactRefs) {
  let [currentState, setCurrentState] = useState(stateMachine.initialState);
  /* ... */
  // I want to omit the refs property from our event here because we are always
  // sending the same values in each event, but this is triggering the error
  // in our send function below.
  function send(event: Omit<TEvent, "refs">) {
    let nodes = Object.keys(refs).reduce(nodeReducer);
    service.send({ ...event, refs: nodes });
  }
  return [currentState, send];
}

function MyComponent({ disabled }) {
  let inputRef = useRef<HTMLInputElement | null>(null);
  let [currentState, send] = useMachine(myStateMachine, { input: inputRef });

  useEffect(() => {
    send({
      type: Events.UpdateContext,
      // Error: Object literal may only specify known properties, and 'context'
      // does not exist in type 'Pick<TEvent, "type">'.
      context: { disabled }
    });
  }, [disabled]);
  /* ... */
}

type NodeRefs = {
  input: HTMLInputElement | null;
};

type ReactRefs = {
  [K in keyof NodeRefs]: React.RefObject<NodeRefs[K]>;
};

我认为您的问题可能是由于内置实用程序类型 Omit<T, K> is, like Pick<T, K>, does not distribute 而不是 T 中的联合引起的。您似乎期望(这并非不合理)Omit<A | B, K> 应该等同于 Omit<A, K> | Omit<B, K>,但它不是那样工作的。相反,Omit<A | B, K> 映射到 keyof (A | B),这与 (keyof A) & (keyof B) 相同,您会发现自己只能看到共享属性。

最简单的解决方法是创建一个 Omit 版本, 确实 通过使用 distributive conditional type 分配给工会。如果你有一个泛型类型参数 T,那么当 T 是联合类型时,构造 T extends any ? F<T> : never 最终会将 F<> 操作分布在 T 上。这是定义:

type DistributiveOmit<T, K extends PropertyKey> = T extends any ? Omit<T, K> : never;

您可以验证 DistributiveOmit<A | B, K> 现在将完全等同于 DistributiveOmit<A, K> | Distributive<B, K>。因此 DistributiveOmit<TEvent, "refs"> 本身就是一个联合:

function foo(event: DistributiveOmit<TEvent, "refs">) {
  switch (event.type) {
    case Events.Set: {
      event.state; // okay
      break;
    }
    case Events.Toggle: {
      break;
    }
    case Events.UpdateContext: {
      event.context; // okay
    }
  }
}

好的,希望对您有所帮助;祝你好运!

Playground link to code