【问题标题】:TypeGuard function for a key in a generic object通用对象中键的 TypeGuard 函数
【发布时间】:2021-11-10 16:57:22
【问题描述】:

是否可以创建一个 TypeScript 类型保护函数来确定给定键是否在给定(通用)对象中 - 非常类似于 key in obj,但作为功能类型保护(与此问题无关的原因需要) .

例如,像这样的:

export function has<T extends { [index: string]: any; [index: number]: any }>(
  obj: T,
  property: string | symbol | number
): property is keyof T {
  return Object.prototype.hasOwnProperty.call(obj, property)
}

// Then in user land somewhere:

interface Foo {
    bar: string
}

interface Fuzz {
    buzz: string
}

function doWork(thing: Foo | Fuzz) {
  if (has(thing, 'bar')) {
    alert(thing.bar) // ideally we've type narrowed to know thing contains foo
  }
}

上面的代码不能像我预期的那样工作 (alert(thing.foo)) 不知道 foo 存在 - 显然我的类型保护声明 property is keyof T 没有达到我的预期。

您可以输入 guard 结果仅为 FooFuzz - 但我特别想输入 guard 某个特定键存在于泛型上。

Playground

【问题讨论】:

  • this 是否满足您的需求?您的类型保护功能正在缩小property,但您已经确切知道"bar" 是什么。您的用例似乎是您想缩小 obj 的范围。我的链接中的has 版本仅在obj 是联合类型时才有效。如果这适用于您的用例,我会写一个答案。如果没有,请修改问题以详细说明您的用例,我会看看它。让我知道任何一种方式,祝你好运。
  • 这是一个聪明的解决方案。我真的希望 obj in 成为任何类型的对象,而不仅仅是联合。不过,这似乎非常接近。
  • 如果您有 minimal reproducible example 的内容不适用于我的解决方案,我可能会更新解决方案。如果没有示例,就很难预测您真正关心的用例。
  • Here's an example 的不良影响,obj 缩小为 never
  • 你为什么要对带有字符串索引签名的东西进行任何缩小,你希望它缩小到什么?看来您应该像x.buzz ??= 123 一样直接访问x.buzz。不管怎样,用你的用例试试this version,让我知道;我已经放弃了明确缩小联合,而是使用了交叉点。请记住将您关心的任何用例添加到问题本身,以便答案不依赖于 cmets 的状态。

标签: typescript typescript-generics


【解决方案1】:

我们希望has(obj, k) 充当user-defined type guard function。我们试图通过has(obj, k) 支持的用例是:

  • 如果k 是单个literal typeobj 是对象类型的union,其中一些联合成员明确地将k 作为键,那么true 结果应该缩小obj 仅适用于以 k 为键的成员,false 结果应将 obj 缩小为仅没有 k 作为键的成员。这就是目前the in operator 通过k in obj 缩小对象的方式。它不是sound,因为structural subtyping 意味着{x: string} 类型的对象很可能拥有比x 更多的键,因此当您检查例如,y 的键。但这就是 in 运算符今天的工作方式,所以我们不妨对 has() 做同样的事情。

  • 如果k 是单一文字类型并且obj不是联合类型,并且如果该非联合类型在键k 处没有显式属性值,则true 结果应该缩小obj 以在键k 处具有显式unknown 属性值。 false 结果根本不应该缩小 obj。目前这不是 TypeScript 中的 in 运算符范围,尽管 microsoft/TypeScript#21732 有建议支持这一点。

  • 如果k 是一个宽属性类型,例如stringnumbersymbolPropertyKey,那么我们根本不想缩小obj 的范围以获取@987654366 @ 或 false 结果。 可能在这种情况下,人们可能想要缩小k 的范围,但这并没有被明确指出为一个用例,所以我不会继续这样做。现在我会说,在这种情况下,has() 的返回值将只是boolean

  • 如果k 是文字类型的联合,那么大概我们希望以与前两种情况相同的方式缩小obj:如果obj 是联合然后缩小向下 obj 到只有那些有/没有与k 的任何可能值匹配的显式键的联合成员;如果obj 不是联合,则将obj 缩小为对象类型的联合,k 的联合的每个可能成员作为结果的一个成员。这没有明确说明为一项要求,但这样做比我能想到的任何其他事情都要好(has() 返回true 的最简单实现会将obj 缩小为具有all的东西来自k 联合的密钥,这要糟糕得多)。


考虑到这些用例,下面是has() 的潜在实现:

export function has<T extends object, K extends PropertyKey>(
    obj: T,
    property: RequireLiteral<K>
): obj is T & { [P in K]: { [Q in P]: unknown } }[K];
export function has(obj: any, property: PropertyKey): boolean;
export function has(obj: any, property: PropertyKey) {
    return Object.prototype.hasOwnProperty.call(obj, property)
}

type RequireLiteral<K extends PropertyKey> =
    string extends K ? never :
    number extends K ? never :
    symbol extends K ? never :
    K

这是一个overloaded function,其中第一个调用签名仅在property 是文字类型或文字类型联合的情况下被调用。如果是,RequireLiteral&lt;K&gt; 类型函数将返回 K,否则将返回 never。在任何情况下,返回类型谓词类型将obj 缩小为其原始类型的intersection,以及在K 中的每个键处具有unknown 属性的类型。 {[P in K]:{[Q in P]:unknown}}[K] 类型可能更容易通过示例来描述:如果 K"a",那么它是 {a: unknown};如果K"a" | "b",那么它就是{a: unknown} | {b: unknown}。此调用签名应导致我们想要支持的所有行为,其中 property 不是宽类型。

仅当propertystringPropertyKey 等宽类型时才调用第二个调用签名。如果是这样,则该函数不会充当类型保护。


我们可以验证所述示例是否按预期工作:

function doWork(thing: Foo | Fuzz) {
    if (has(thing, 'bar')) {
        // Foo
        thing.bar
    } else {
        // Fuzz
        thing.buzz
    }
}

const x: { [index: string]: any } = {
    baz: 123
}
if (!has(x, 'buzz')) {
    /*  { [index: string]: any; } */
    x.buzz = 123        
} else {
    /* { [index: string]: any; } & { buzz: unknown; } */
    x.buzz 
}


const y: { [index: string]: any } = {}
const key: PropertyKey = 'a'
if (!has(y, key)) {
    // { [index: string]: any; }
    y[key] = 123 
} else {
    // { [index: string]: any; }
    y
}

看起来不错!


Playground link to code

【讨论】:

  • 很好的类型保护实现!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-14
  • 2021-05-26
  • 2018-07-26
  • 1970-01-01
  • 2020-03-19
  • 1970-01-01
相关资源
最近更新 更多