来自已区分联合数组的查找的窄返回类型

时间:2019-11-26 11:35:49

标签: typescript type-conversion discriminated-union

我经常使用下面的示例中的代码,并且想知道是否存在一些无需进行显式类型声明就可以键入find结果的聪明方法。

type Foo = { type: "Foo" };
type Goo = { type: "Goo" };
type Union = Foo | Goo;

const arr: Union[] = [];
const foo = arr.find(a => a.type === "Foo") as Foo;

如果省略了as Foo类型的断言,则结果将是Union类型的,即使它只能返回类型Foo

在这样的示例中,修复find类型以返回缩小的类型的最干净方法是什么?

编辑:此问题也可能适用于filter和其他类似方法。

Edit2:建议的类似问题的接受答案(Way to tell TypeScript compiler Array.prototype.filter removes certain types from an array?}显示,通过在find/filter的谓词中使用类型保护,可以缩小返回值。

如果,例如,这种类型的防护功能应如何缩小受歧视的联合的范围区分字符串文字总是在type键下吗?

4 个答案:

答案 0 :(得分:1)

如果您希望生成用于用户定义类型保护函数的生成器,以返回区分已区分联合的类型谓词,则它可能看起来像这样:

function discriminate<K extends PropertyKey, V extends string | number | boolean>(
    discriminantKey: K, discriminantValue: V
) {
    return <T extends Record<K, any>>(
        obj: T & Record<K, V extends T[K] ? T[K] : V>
    ): obj is Extract<T, Record<K, V>> =>
        obj[discriminantKey] === discriminantValue;
}

如果我调用discriminate("type", "Foo"),则结果是签名类似于<T>(obj: T)=>obj is Extract<T, {type: "Foo"}>的函数。 (我说这是相似,因为实际的返回值将T限制为仅以"type"作为键的类型,并且您可以将"Foo"分配给它。)看看它是如何工作的:

const foo = arr.find(discriminate("type", "Foo")); // Foo | undefined 
const goos = arr.filter(discriminate("type", "Goo"));  // Goo[]

看起来不错。如果您传递了不适用的字段/值,则会发生以下情况:

const mistake1 = arr.find(discriminate("hype", "Foo")); // error!
// ------------------->   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Union is not assignable to Record<"hype", any>.
const mistake2 = arr.find(discriminate("type", "Hoo")); // error!
// ------------------->   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Union is not assignable to ((Foo | Goo) & Record<"type", "Hoo">)

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

Link to code

答案 1 :(得分:0)

这是上面答案中的jcalz的代码,它带有否定和联合。

export function isDiscriminate<K extends PropertyKey, V extends string | number | boolean>(
    discriminantKey: K, discriminantValue: V | V[]
) {
    return <T extends Record<K, any>>(
        obj: T & Record<K, V extends T[K] ? T[K] : V>
    ): obj is Extract<T, Record<K, V>> =>
        Array.isArray(discriminantValue) 
            ? discriminantValue.some(v => obj[discriminantKey] === v)
            : obj[discriminantKey] === discriminantValue;
}

export function isNotDiscriminate<K extends PropertyKey, V extends string | number | boolean>(
    discriminantKey: K, discriminantValue: V | V[]
) {
    return <T extends Record<K, any>>(
        obj: T & Record<K, V extends T[K] ? T[K] : V>
    ): obj is Exclude<T, Record<K, V>> =>
        Array.isArray(discriminantValue)
            ? discriminantValue.some(v => obj[discriminantKey] === v)
            : obj[discriminantKey] === discriminantValue;
}

和用法:

type A = { type: "A" };
type B = { type: "B" };
type C = { type: "C" };
type Union = A | B | C;

const arr: Union[] = [];
arr.find(isDiscriminate("type", "A")); // A
arr.find(isDiscriminate("type", ["A", "B"])); // A | B
arr.find(isNotDiscriminate("type", "A")); // B | C
arr.find(isNotDiscriminate("type", ["A", "B"])) // C

答案 2 :(得分:0)

公认的答案很好,但是很难理解。我去了这样的事情。它的可重用性较低,仅适用于一种类型,但更易于阅读。

function isType<V extends Union['type']>(val: V) {
  return (obj: Union):
      obj is Extract<Union, {type: V}> => obj.type === val;
}

const found = arr.find(isType('Foo'));

答案 3 :(得分:0)

感谢jcalz购买the excellent accepted answer

我为谓词警卫注释了一家工厂的略有不同:

function hasProp<K extends PropertyKey, V extends string | number | boolean>(k: K, v: V) {
  
  // All candidate types might have key `K` of any type
  type Candidate = Partial<Record<K, any>> | null | undefined

  // All matching subtypes of T must have key `K` equal value `V`
  type Match<T extends Candidate> = Extract<T, Record<K, V>>

  return <T extends Candidate>(obj: T): obj is Match<T> => (
    obj?.[k] === v
  )
}