【问题标题】:TypeScript: defining the `in` operator as a functionTypeScript:将“in”运算符定义为函数
【发布时间】:2019-11-07 23:12:53
【问题描述】:

我想创建一个行为与in 运算符完全相同的函数,其中使用user-defined type guards 缩小类型。

(例如,请参阅Lodash's has function。)

对于 n in x 表达式,其中 n 是字符串文字或字符串文字类型,x 是联合类型,“true”分支缩小为具有可选或必需属性 n 的类型,“false”分支缩小具有可选或缺失属性 n 的类型。

https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-the-in-operator

我编写了一些测试,展示了 in 运算符的行为以及我们的 has 函数的预期行为。如何定义一个函数 (has) 使其行为与这些测试中的 in 运算符完全相同?

declare const any: any;

type Record = { foo: string; fooOptional?: string };
type Union = { foo: string; fooOptional?: string } | { bar: number; barOptional?: string };

{
  const record: Record = any;

  if ('foo' in record) {
    record; // $ExpectType Record
  } else {
    record; // $ExpectType never
  }

  if (has(record, 'foo')) {
    record; // $ExpectType Record
  } else {
    record; // $ExpectType never
  }
}

{
  const union: Union = any;

  if ('foo' in union) {
    union; // $ExpectType { foo: string; fooOptional?: string | undefined; }
  } else {
    union; // $ExpectType { bar: number; barOptional?: string | undefined; }
  }

  if (has(union, 'foo')) {
    union; // $ExpectType { foo: string; fooOptional?: string | undefined; }
  } else {
    union; // $ExpectType { bar: number; barOptional?: string | undefined; }
  }
}

{
  const unionWithOptional: { foo: string } | { bar?: number } = any;

  if ('bar' in unionWithOptional) {
    unionWithOptional; // $ExpectType { bar?: number | undefined; }
  } else {
    unionWithOptional; // $ExpectType { foo: string; } | { bar?: number | undefined; }
  }

  if (has(unionWithOptional, 'bar')) {
    unionWithOptional; // $ExpectType { bar?: number | undefined; }
  } else {
    unionWithOptional; // $ExpectType { foo: string; } | { bar?: number | undefined; }
  }
}

我最接近解决这个问题的是with this

type Discriminate<U, K extends PropertyKey> = U extends any
  ? K extends keyof U
    ? U
    : U & Record<K, unknown>
  : never;

export const has = <T extends object, K extends PropertyKey>(
  source: T,
  property: K,
): source is Discriminate<T, K> =>
  property in source;

不幸的是,最后一次测试没有通过。

对于上下文,我想这样做的原因是我的自定义 has 函数可以在编译时验证键(in 运算符不这样做)。我可以弄清楚这部分——我正在努力解决的部分只是模仿in 进行的缩小。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    如果没有one-sided or fine-grained type guards 支持守卫真假双方的独立行为,目前是不可能的。


    您要求的是采用联合类型 { foo: string } | { bar?: number } 并提出用户定义的类型保护,其中真分支将其缩小为 { bar?: number },而假分支将其保留为完整联合 @987654326 @。

    阅读编译器的类型检查器checker.ts的第19788到19809行中的getNarrowedType()函数,我认为这不可能发生。

    现在,将(x: any) =&gt; x is G 类型的用户定义类型保护应用于union 类型T 的值,编译器当前将分区 联合type 用于类型保护的真分支和假分支。所以在真分支中你会得到类似Extract&lt;T, G&gt;的东西,在假分支中你会得到类似Exclude&lt;T, G&gt;的东西。如果一个联合成员存在于真分支中,它将不存在于假分支中,反之亦然。

    只有当T 不是联合类型时 真分支和假分支缩小才可能不相互排斥(有时你会得到真分支的T &amp; G并且只是 T 用于错误分支)。

    这是not perfect,但它就是这样。


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

    【讨论】:

      【解决方案2】:

      has 函数的编写方式可以缩小其参数的类型,至少在键是可选的并且可以缩小为非可选的情况下:

      function has<T,K extends keyof T>(obj: T, key: K):
          obj is T & Record<K, Exclude<T[K], undefined>>
      {
          return obj[key] !== undefined;
      }
      
      type Example = { a: number, b?: number };
      let obj: Example = { a: 1, b: 2 };
      
      // inferred obj.b : number|undefined
      
      if(has(obj, 'b')) {
          // inferred obj.b : number
      }
      

      但是,您对 Union 类型的要求并不合理。考虑这个例子:

      type UnsoundUnion = { a: number, b: number } | { c: number, d: number };
      
      let u: UnsoundUnion = { a: 1, c: 2, d: 3 };
      
      if(has(u, 'a')) {
          // it would be wrong to infer u.b : number
      }
      

      【讨论】:

      • “推断 u.b : number 是错误的”——但这就是 in 运算符所做的。我只对模仿 in 运算符的行为感兴趣。
      • 在您的示例中,in 运算符不仅仅推断您使用in 测试的foo 属性的类型 - 您还希望它还推断fooOptional 为@987654332 @。这种推论一般来说是不合理的,因为 foo 属性的存在并不意味着对象的类型来自联合的那个分支。
      • 我知道这可能不合理,但这并不真正适用于我的问题?我希望 has 的行为与 TypeScript 中的 in 运算符完全一样(即使这可能不合理)——正如我的测试所证明的那样,这两种方法都展示了(两者都有相同的期望)。
      • 我怀疑这是不可能的,因为它需要以与in 似乎相同的方式破坏另一种语言结构。我还建议以依赖于不健全的类型推断的方式编写代码,因为如果在以后的版本中修复了此问题,您的代码将不再进行类型检查。是否可以使用标记的联合来做你想做的事情?
      • IIUC,这种“不健全”来自于 TypeScript 允许在没有判别属性的联合中使用多余的属性:github.com/microsoft/TypeScript/issues/20863。这可能会在未来发生变化,但这不会改变 in 运算符的行为。
      猜你喜欢
      • 2021-05-14
      • 2019-12-26
      • 2015-01-30
      • 1970-01-01
      • 2015-05-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多