从有区别的联合数组中查找的窄返回类型

Mar*_*ťko 4 type-conversion discriminated-union typescript

我经常使用下面示例中的代码,并且想知道是否有一些聪明的方法来输入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;
Run Code Online (Sandbox Code Playgroud)

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

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

编辑:这个问题也适用于filter和其他类似的方法。

Edit2:建议类似问题的已接受答案(Way to tell TypeScript compiler Array.prototype.filter 从数组中删除某些类型?)表明通过在谓词中使用类型保护find/filter可以缩小返回值的范围。

如果例如区分字符串文字总是在type键下,这个类型保护函数应该如何缩小任何可区分的联合?

jca*_*alz 7

如果你想要一个用于用户定义的类型保护函数的生成器返回一个区分可区分联合的类型谓词,它可能看起来像这样:

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;
}
Run Code Online (Sandbox Code Playgroud)

如果我调用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[]
Run Code Online (Sandbox Code Playgroud)

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

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">)
Run Code Online (Sandbox Code Playgroud)

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

代码链接


Tre*_*xon 6

接受的答案非常好,但很难理解。我带着这样的东西去了。它的可重用性较差,仅适用于一种类型,但更易于阅读。

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'));
Run Code Online (Sandbox Code Playgroud)


Mar*_*ťko 5

这是上面答案中 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;
}
Run Code Online (Sandbox Code Playgroud)

和用法:

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
Run Code Online (Sandbox Code Playgroud)


小智 4

现在我们可以将它作为一个相当通用的实用程序:

export function isOfType<
  GenericType extends string,
  Union extends { type: GenericType },
  SpecificType extends GenericType,
>(val: SpecificType) {
  return (obj: Union): obj is Extract<Union, { type: SpecificType }> =>
    obj.type === val;
}

const found = arr.find(isOfType('Foo'));
Run Code Online (Sandbox Code Playgroud)

灵感来自 Trevor Dixon,来自/sf/answers/4277193931/