打字稿与联合的交集导致不存在的属性

Typescript intersection with a union leads to non-existent properties

在下面的示例中,我定义了 Typescript 类型以从索引请求数据。

有两种高效的方法可以从索引服务器检索数据块,或者 startKey endKey or by startKey,limit (按键数).

我在组合这些替代案例以在 Typescript 中定义请求时做错了什么,我看不出是什么,除非我的相交联合的方法没有意义或者我不理解 typescript 错误。

interface StartKey {
  startKey: string;
}

interface EndKey {
  endKey: string;
}

interface Limit {
  limit: number;
}

type KeyRange = StartKey & EndKey;

type KeyLimit = StartKey & Limit;

type KeyBounds = KeyRange | KeyLimit;

export type Options = {
    someparam:string
} & KeyBounds;

function retrieve(options:Options){
    const {
        startKey,
        endKey, //this line causes a compiler error
        limit, //this line causes a compiler error
    } = options;
} 

首先我创建了两个备用接口KeyRange(其中有endKey)和 KeyLimit(具有 limit)。然后我将这些接口合并为 KeyBounds 类型。该 KeyBounds 类型然后在编写请求时通过与其他索引请求特定参数的交集组合。例如,使用 Options 请求项目应该能够使用一种或另一种策略来限制返回的结果。

This playground 显示了我目前采用的方法以及我从选项定义中得到的令人惊讶的(对我来说)错误...

我希望有 some 获取 endKey 或限制的路径,因为 Options 包括具有这些属性的类型的联合.最多它们中的一个会在任何时候出现,但这就像有一个可选的 属性,它不会引发编译器错误。

导致错误的解构正是在我试图明确验证请求了哪个备用键边界签名时,(我希望一个或另一个 属性 未设置)。

相比之下,这段明确可选的代码将解构 NOT 视为错误情况,即使 endKey 和 limit 对于任何特定对象都可能未定义。我期待与联合的交集产生类似的数据结构,除了编译器知道可能有一个 endKey XOR 限制。

interface KeyRange {
  startKey:string
  endKey?:string
  limit?:string
}

function retrieve(range:KeyRange){
  const {
    startKey,
    endKey,
    limit,
  } = range;
}

得到一个在结果类型上根本不存在的错误(甚至不是可选的)让我感到惊讶,这表明我错过了一些东西。谁能告诉我我需要做什么才能使这些替代项有效?

一般情况下,您无法访问联合类型值上的 属性,除非已知 属性 键存在于联合的 每个 成员中:

interface Foo {
  foo: string;
}
interface Bar {
  bar: string;
}
function processFooOrBar(fooOrBar: Foo | Bar) {
  fooOrBar.foo; // error!
  // Property 'foo' does not exist on type 'Foo | Bar'.
  // Property 'foo' does not exist on type 'Bar'
}

错误信息有点误导。当编译器抱怨“属性 foo 在类型 Foo | Bar 上不存在”时,它实际上意味着“属性 foo 已知 存在于 Foo | Bar 类型的值中”。 属性当然有可能存在,但是因为Bar类型的值不一定有这样的属性,编译器会警告你


如果你有一个联合类型的值并且想访问仅存在于联合的某些成员上的属性,你需要做一些narrowing of the value via a type guard. For example, you can use the in operator as a type guard (as implemented by microsoft/TypeScript#15256):

  if ("foo" in fooOrBar) {
    fooOrBar.foo.toUpperCase(); // okay
  } else {
    fooOrBar.bar.toUpperCase(); // okay
  }

在你的情况下,这意味着将你的解构分为两种情况:

  let startKey: string;
  let endKey: string | undefined;
  let limit: number | undefined;
  if ("endKey" in options) {
    const { startKey, endKey } = options;
  } else {
    const { startKey, limit } = options;
  }

(这个 in 类型保护很有用,但在技术上不安全,因为对象类型在 TypeScript 中是开放和可扩展的。可以用 foo 属性 像这样:

const hmm = { bar: "hello", foo: 123 };
processFooOrBar(hmm); // no error at compiler time, but impl will error at runtime

所以要小心......但实际上这种情况很少发生)


处理此问题的另一种方法是在进行解构之前扩展为具有显式可选属性的类型。您已经将此作为解决方法,但您不需要触摸 Options 类型本身。只需将 options 值从 Options 扩大到 StartKey & Partial<EndKey & Limit>:

const widerOptions: StartKey & Partial<EndKey & Limit> = options;    
const {
  startKey,
  endKey,
  limit,
} = widerOptions;

最后,您可以将 Options 重写为明确的“异或”版本,编译器知道如果您在并集的“错误”一侧检查 属性,则该值将为undefined:

type XorOptions = {
  startKey: string,
  endKey?: never,
  limit: number,
  someParam: string
} | {
  startKey: string,
  endKey: string,
  limit?: never,
  someParam: string
}

这与您的 Options 不同,因为 XorOptions 联盟的每个成员都明确提及每个 属性。然后你可以毫无问题地解构:

function retrieve2(options: XorOptions) {
  const {
    startKey,
    endKey,
    limit,
  } = options;
}

Playground link to code

基于@jcalz 的建议,我定义了 KeyBounds,它可以通过引入 Xor类型。这会强制 unused 路径(A 或 B)中的 属性 名称具有类型 never 而不是简单地没有定义。

一旦有了定义,即使它有时解析为 never,那么解构也可以毫无错误地发生...

type Xor<A, B> =
  | (A & { [k in keyof B]?: never })
  | (B & { [k in keyof A]?: never });

interface StartKey {
  startKey: string;
}

interface EndKey {
  endKey: string;
}

interface Limit {
  limit: number;
}

type KeyBounds = StartKey & Xor<EndKey, Limit>;

export type Options = {
    someparam:string
} & KeyBounds;

function retrieve(options:Options){
    const {
        startKey,
        endKey,
        limit,
    } = options;
}