Narrow discriminated union using nested key

问题描述 投票:0回答:1

我正在尝试将已区分的联合数组过滤为其联合类型的单独数组。我偶然发现了this question它给出了通用的鉴别器。

不幸的是,我正在使用的数据在最高级别没有区分键。

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

我通过更改另一篇文章中的鉴别器函数来修复此问题,以期望“元”对象降低一级。

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

有没有更通用的方法来做到这一点?也许通过使用像

discriminateNested("meta.type", "Foo")
这样的字符串指定嵌套键?

typescript filter type-conversion discriminated-union
1个回答
0
投票

一种可能的方法是

discriminateNested()
获取键的 tuple 并根据相应嵌套属性的值进行区分。你可以用点分隔的字符串来代替,但这种方式在打字方面更直接一些。

首先我们应该定义

NestedRecord<K, V>
,其中
K
是一个键元组。我们希望
NestedRecord<["a", "b", "c"], string>
等同于
{a: {b: {c: string}}}
。它可能看起来像这样:

type NestedRecord<K extends PropertyKey[], V> =
  K extends [infer K0 extends PropertyKey, ...infer KR extends PropertyKey[]] ?
  { [P in K0]: NestedRecord<KR, V> } : V;

这是一个 递归条件类型,它使用 可变元组类型 来解析

K
并构建嵌套类型。

现在

discriminatedNested()
可以这样实现:

function discriminateNested<K extends PropertyKey[], V extends string | number | boolean>(
  discriminantKeys: [...K], discriminantValue: V
) {
  return <T extends NestedRecord<K, string | number | boolean>>(
    obj: T
  ): obj is Extract<T, NestedRecord<K, V>> =>
    discriminantKeys.reduce<any>((acc, k) => acc[k], obj) === discriminantValue;
}

这需要

discriminantKeys
的元组类型
K
(好吧,
[...K]
,这只是暗示你希望它是一个元组而不是无序数组;参见 microsoft/TypeScript#39094 的描述,它实现了可变参数元组类型)和
discriminant value
类型的
V
,并返回自定义类型保护函数,其中
Extracts
输入类型
T
的联合成员仅分配给那些可分配给
Nestedrecord<K, V>>
的实现在reduce()
上使用
discriminantKeys
数组方法
来执行嵌套索引。


让我们测试一下:

type Foo = { meta: { type: "Foo" }, a: string };
type Goo = { meta: { type: "Goo" }, b: number };
type Union = Foo | Goo;

const discFoo = discriminateNested(["meta", "type"], "Foo");
/* const discFoo: <T extends {
    meta: {
        type: string | number | boolean;
    };
}>(obj: T) => obj is Extract<T, {
    meta: {
        type: "Foo";
    };
}> */

const u: Union = Math.random() < 0.5 ?
  { meta: { type: "Foo" }, a: "abc" } : { meta: { type: "Goo" }, b: 123 };

if (discFoo(u)) {
  u // Foo
  console.log(u.a.toUpperCase());
} else {
  u // Goo
  console.log(u.b.toFixed(2));
}

看起来不错。

discFoo
类型的保护函数接受任何具有
{meta:{type: string | number | boolean}}
类型的输入,并过滤掉那些可分配给
{meta:{type:"Foo"}}
的联合成员。当你给它传递一个
Union
类型的值时,真分支缩小到
Foo
,假分支缩小到
Goo
,根据需要。

游乐场代码链接

© www.soinside.com 2019 - 2024. All rights reserved.