TypeScript:将“in”操作符定义为函数

b4lqfgs4  于 2023-05-01  发布在  TypeScript
关注(0)|答案(2)|浏览(113)

我想创建一个行为与in操作符完全相同的函数,其中使用用户定义的类型保护来缩小类型。
(For一个例子,参见Lodash的has函数。)
对于x表达式中的n,其中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; }
  }
}

我最接近解决这个问题的方法是:

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执行的收缩。

izj3ouym

izj3ouym1#

如果没有单边或细粒度类型保护(如microsoft/TypeScript#14048中所要求的),支持保护的true和false端的独立行为,目前是不可能的。
你所要求的是接受一个联合类型{ foo: string } | { bar?: number },并提出一个用户定义的类型保护,其中真分支将其缩小为{ bar?: number },假分支将其保留为完整的联合{ foo: string } | { bar?: number }
阅读checker.ts的第19788到19809行中的getNarrowedType()函数,编译器的类型检查器,我认为这不可能发生。
现在,将用户定义的(x: any) => x is G类型的类型保护应用于 union type T的值,编译器当前将对类型保护的true分支和false分支的union类型进行 * 分区。所以在true分支中你会得到类似于Extract<T, G>的东西,在false分支中你会得到类似于Exclude<T, G>的东西。如果一个联合成员存在于真分支中,它将不存在于假分支中,反之亦然。
只有当Tnot 联合类型时,真分支和假分支窄化才可能不互相排斥(有时你会得到真分支的T & G,而假分支只有T)。
这并不完美(按照ms/TS#31156),但它就是这样。

iyr7buue

iyr7buue2#

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
}

相关问题