如何通过传递排除项(keyof T)[]来缩小对象T的类型?

时间:2019-03-08 15:56:16

标签: typescript typescript-typings

我如何描述可用作类型防护以从对象中排除键的类型?

下面,我有一个函数getExcludedKeys,该函数从传入的对象中筛选出键。 我遇到的是类型防护什么也没做,我获得了所有3个属性,我想要做的就是缩小类型的范围,以便从类型中排除过滤的键。

我以前在filter表达式中使用过类型保护,但从未在预定义数组中使用过。

interface Foo {
  id: number
  val1: string
  val2: string
}

type KeysOf<T> = (keyof T)[]
const getKeys = <T> (obj: T) => Object.keys(obj) as KeysOf<T>
const getExcludedKeys = <T> (obj: T, excludeKeys: KeysOf<T>) =>
  getKeys(obj)
    .filter((key): key is Exclude<keyof T, typeof excludeKeys> => { // This line isn't working as expected.
      return !excludeKeys.includes(key)
    })


const foo: Foo = {
  id: 1,
  val1: 'val1',
  val2: 'val2'
}

const result = getExcludedKeys(foo, ['val1', 'val2'])
  .map(key => key)  // EXPECTED :: key: "id"
                    // ACTUAL   :: key: "id" | "val1" | "val2"

2 个答案:

答案 0 :(得分:2)

您给出了一个自我解答,然后悬赏于您的问题并将此评论添加到您的回答中:

  

如果在传递给排除对象之前在变量/常量中定义了排除键的数组,这一切都会中断。

所以我假设这是您关注的问题。确实,如果我采用您的代码,然后执行此操作,则会在exclude的第二个参数上出现错误:

const toExclude = ["foo", "bar"];
const b = exclude(a, toExclude)
    .map(key => {                   // (EXPECTED :: key: 'baz') (ACTUAL :: key: 'foo' | 'bar' | 'baz')
        if (key === 'bar') { }      // Error
        if (key === 'foo') { }      // Error
        if (key === 'baz') { }      // Okay
        if (key === 'yay') { }      // Okay
    });

您的问题是,当您声明这样的变量时:

const toExclude = ["foo", "bar"];

TypeScript将推断“最佳通用类型”(此术语来自documentation)。 “最佳通用类型”是适合最普通使用情况的类型。通常,当有人定义包含字符串文字的数组时,所需的推断类型是string[]。这类似于您进行const x = "moo";的推断方式时,x的类型为string而不是"moo"。如果要使用更窄的类型,则需要明确使用const x: "moo" = "moo"。同样,const x = 1可以推断number的{​​{1}},尽管它的类型可能更窄。

对您来说,这是一个问题,因为您的数组必须包含作为传递给x的第一个参数上可能键的子集的值。 您必须告诉TS,您的意思是数组具有更窄的类型。您可以执行以下操作:

exclude

如果您有多个数组,则需要这样输入,每次必须添加类型断言可能会变老。您可以使用辅助函数来避免每次都要键入类型断言。例如:

const toExclude = ["foo", "bar"] as ["foo", "bar"];
上面的

function asKeysOfA<T extends (keyof typeof a)[]>(...values: T) { return values; } const toExclude = asKeysOfA("foo", "bar"); 推断为类型toExclude。这是一个适用于任何对象类型而不只是["foo", "bar"]的版本:

typeof a

在这种情况下,您需要传递function asKeysOf<O, T extends (keyof O)[]>(o: O, ...values: T) { return values; } const toExclude = asKeysOf(a, "foo", "bar"); 作为第一个参数,以便可以正确推断aT不被该功能使用。

您可能想知道为什么在执行a时不需要执行类型声明,因为在推断第二个参数的类型时,TS使用的方法不同于“最佳通用类型”。它确定“上下文类型”。如果结构可以满足为exclude(a, ["foo", "bar"])的第二个参数声明的类型,则TS会推断该类型,而不是更一般的类型,因此您不必使用类型断言。

这实际上是上述excludeasKeysOfA函数所利用的:将值作为参数传递给这些函数时,TS会为它们推断上下文类型,然后在推断返回值时继续使用辅助功能的值。

我将在此处添加一个从您的代码派生的完整示例,其中包括一些辅助函数,这些函数为变量asKeysOf提供了一个很好的窄元组类型。我发现toExcludeKeysList类型声明没有太大的好处。对我来说,它们不会使代码更易于阅读。所以我删除了它们。 Omit的输入原理仍然相同。

exclude

TypeScript 3.4即将发布,并提供一些新功能,这些功能会影响您在此处尝试执行的操作。在TypeScript 3.4中,可以要求编译器推断出尽可能窄的类型用于文字值,否则将推断出“最佳通用类型”。您必须使用类型断言function getAllKeys<T>(obj: T): (keyof T)[] { return Object.keys(obj) as (keyof T)[]; } function exclude<T, K extends (keyof T)[]>(obj: T, excludes: K) { return getAllKeys(obj) .filter((key: keyof T): key is Exclude<keyof T, K[number]> => !excludes.includes(key)); } const a = { foo: 'abc', bar: 'abc', baz: 'abc', yay: true }; // function asKeysOfA<T extends (keyof typeof a)[]>(...values: T) { return values; } // const toExclude = asKeysOfA("foo", "bar"); function asKeysOf<O, T extends (keyof O)[]>(o: O, ...values: T) { return values; } const toExclude = asKeysOf(a, "foo", "bar"); const b = exclude(a, toExclude) .map(key => { // (EXPECTED :: key: 'baz') (ACTUAL :: key: 'foo' | 'bar' | 'baz') if (key === 'bar') { } // Error if (key === 'foo') { } // Error if (key === 'baz') { } // Okay if (key === 'yay') { } // Okay }); 。因此,as const数组可以这样声明:

toExclude

请注意,这也将使数组const toExclude = ["foo", "bar"] as const; 。因此,readonly的声明也应该包含一个exclude数组,因此readonly的定义必须更改为K

K extends readonly ...

这是一个例子:

function exclude<T, K extends readonly (keyof T)[]>(obj: T, excludes: K) {

此示例在Typescript 3.4.0-rc中的行为符合预期

答案 1 :(得分:1)

经过数小时的纠缠,我终于设法在@nucleartux的帮助下使用Omit类型将其破解。

我所需要的只是将这种古怪的类型Omit<T, K extends KeysList<T>> = Exclude<keyof T, K[number]>用作类型保护,并结合具有第二种泛型类型exclude的{​​{1}}

K extends (keyof T)[]