我有一个包含嵌套元组的元组对象

const foo = [
  { id: 't1', values: ['a', 'b'] },
  { id: 't2', values: ['a', 'c'] },
  { id: 't3', values: ['b', 'c'] },
] as const;

我希望能够根据元组值进行过滤,但也要区分类型.

此函数将为您提供正确的运行时值,但会出现类型错误

var objsWithA = foo.filter(({ values }) => values.includes('a'));

如果使用as anyas never绕过类型错误,则结果类型不会消除t3

var objsWithA = foo.filter(({ values }) => values.includes('a' as never));

是否可以获得与返回值匹配的结果类型? IE

[
  { id: 't1', values: ['a', 'b'] },
  { id: 't2', values: ['a', 'c'] },
]

ts playground link

推荐答案

TypeScrip通过调用this call signature,在一定程度上支持使用the array filter() methodnarrow的结果数组类型:

interface ReadonlyArray<T> {
  filter<S extends T>(
    predicate: (value: T, index: number, array: readonly T[]) => value is S,
    thisArg?: any
  ): S[];
}

这意味着predicate参数需要是返回类型为type predicatecustom type guard function.

不幸的是,类型脚本不支持infer个来自函数实现的类型谓词,即使在像`x=>;typeof x="字符串"这样的简单情况下也是如此.在microsoft/TypeScript#38390岁的时候,有一个悬而未决的问题需要这样的支持.

即使发生了这种情况,它也可能不适用于({values})=>values.includes("a");即使直接拨打the includes() array method也不能起到打字保护的作用.这个请求在microsoft/TypeScript#31018中被拒绝了,因为它太复杂了,无法正确处理.

这意味着您必须使用显式编写为定制类型保护函数的回调来调用filter(),并明智地使用它.以下是一种生成类型保护函数的可能方法,该方法适用于比您已有的情况稍微更一般的情况:

const objPropHasArrayContainingStringLiteral =
    <K extends string, T extends string>(
        propName: K,
        stringLiteral: T
    ) => <U extends Record<K, readonly string[]>>(
        obj: U
    ): obj is (
       U extends Record<K, readonly (infer V extends string)[]> ? 
       [T] extends [V] ? U : never : never
    ) => obj[propName].includes(stringLiteral);

这里,objPropHasArrayContainingStringLiteral(propName, stringLiteral)产生类型保护函数,该函数在propName处判断对象是否具有包含stringLiteral的数组属性.

类型输出是使用distributive conditional typeunion type U筛选到其在键K处的属性是包含T个元素的只读数组的那些成员(至少,假设T是字符串literal type,并且U的数组类型本身持有字符串文字类型).如上所述,要做到正确是很棘手的.

因此,对于您的示例,它将是objPropHasArrayContainingStringLiteral("values", "a"):

const objsWithA = foo.filter(objPropHasArrayContainingStringLiteral("values", "a"));

/* const objsWithA: ({
    readonly id: "t1";
    readonly values: readonly ["a", "b"];
} | {
    readonly id: "t2";
    readonly values: readonly ["a", "c"];
})[] */

这很管用,万岁.但我不确定我们在这里解决的问题是否值得采取更普遍的解决方案.如果你只打算这样做一次,那么你不妨只做内联:

const objsWithA = foo.filter(<U extends { values: readonly string[] }>(
    obj: U): obj is U extends { values: readonly (infer V extends string)[] } ?
    "a" extends V ? U : never : never => obj.values.includes("a")
);

这是一样的,只是K硬编码为"values",T硬编码为"a".本质上,它是说回调将U并集过滤到其values属性为只读数组的那些成员,该数组可能包含类型"a"的值.

请注意,编译器实际上并不验证定制类型保护函数的实现.您可以将obj.values.includes("a")更改为!obj.values.includes("a")obj.values.includes("whoops"),编译器不会注意到.所以,再说一次,你必须小心.

Playground link to code

Typescript相关问答推荐

如何在ts中使用函数重载推断参数类型

TypScript如何在 struct 上定义基元类型?

带有微前端的动态React路由问题

TypeScript实用程序类型—至少需要一个属性的接口,某些指定属性除外

忽略和K的定义

如何键入函数以只接受映射到其他一些特定类型的参数类型?

在嵌套属性中使用Required

类型脚本中没有接口的中间静态类

接口中函数的条件参数

从对象类型描述生成类型

在组件卸载时try 使用useEffect清除状态时发生冲突.react +打字

如何避免从引用<;字符串&>到引用<;字符串|未定义&>;的不安全赋值?

将接口映射到交叉口类型

字幕子集一个元组的多个元素

VUE 3类型';字符串';不可分配给类型';参考<;字符串&>

如何使用IsEqual创建断言类型相等的实用程序

Typescript 和 Rust 中跨平台的位移数字不一致

为什么 Typescript 无法正确推断数组元素的类型?

const nav: typechecking = useNavigation() 和 const nav = useNavigation() 之间有什么区别?

是否有解释为什么默认情况下在泛型类型上访问的属性不是索引访问