Keyof 在应用于与 Record<string、unknown) 相交的类型时会丢失已知属性>并导致与 Omit<T、K 一起使用的类型不正确>



我遇到了一个问题,我不知道这是实际的问题还是我这边的误解。

考虑到这段代码

type Props = {
foo: string
bar: string
} & Record<string, unknown>
// Using Record<string, unknown> seems to be the recommended way of saying "any object"
// Looks fine, no errors: the known properties seems to be kept in the intersection
const obj: Props = { foo: '', bar: '' }
type KeysOfProps = keyof Props // Here this type is "string"

对我来说:

  • 如果keyof Props is string,则Props不应允许objtototata作为字符串
  • 如果{ foo: string; bar: string }可分配给Props,则keyof Props应仍然具有foobar的已知属性。(否则foobar在我看来应该是未知的(

看起来不兼容,但我发现keyof Props可能是'foo' | 'bar' | string,它被简化为string

但是,这给我的情况带来了一些问题。例如:

// This is Record<string, unknown>, `bar` is lost in the process
type PropsWithoutFoo = Omit<Props, 'foo'>
// This I can explain by the fact that Omit is described this way:
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// `keyof T` being `string and K being 'foo': Exclude<string, 'foo'> is string
// Pick<Props, string> is 'foo' | 'bar' | unknown which is unknown
// => Every keys of the intersection is lost

这发生在一个带有泛型的代码中,其中Record<string, unknown>实际上是一个默认类型,可以被任何扩展它的类型所取代。我的属性的丢失似乎可以归结为这一点,这就是为什么我需要让它发挥作用。

这里的TypeScript中一切都按预期工作。在我看来,您的要点都是误解:keyof类型运算符在应用于包含字符串索引的类型时是有损的。正如你所指出的,'foo' | 'bar' | string实际上只是string;这是真的,因为Props类型的值可以具有任何string值的键。如果您编写Pick<XXX, keyof XXX>,其中XXX具有字符串索引签名,那么您将因此丢失已知密钥:

type PropsMissingKnownKeys = Pick<Props, keyof Props>;
/* type PropsMissingKnownKeys = {
[x: string]: unknown;
} */

注意,这不同于形式为K in keyof XXX直接映射类型;在这种情况下,编译器将专门处理in keyof,并迭代每个已知的键以及索引器:

type HomomorphicProps = { [K in keyof Props]: Props[K] };
/* type HomomorphicProps = {
[x: string]: unknown;
foo: string;
bar: string;
} */

这导致了Omit的一个可能的不同版本,该版本使用TypeScript 4.1:中引入的对映射类型中的密钥重映射的支持来实现

type AlternativeOmit<T, K extends PropertyKey> = {
[P in keyof T as Exclude<P, K>]: T[P]
}

AlternativeOmit<T, K>是直接映射类型,它迭代T中的每个属性密钥P,并根据P是否可分配给K将每个密钥重新映射到neverP。如果您使用它而不是Omit,则会出现所需的类型:

type PropsWithoutFoo = AlternativeOmit<Props, "foo">;
/* type PropsWithoutFoo = {
[x: string]: unknown;
bar: string;
} */

由于AlternativeOmitOmit具有不同的行为,因此很可能存在边缘情况,其中Omit的行为符合您的要求,而AlternativeOmit则不然。所以我不会说你应该用AlternativeOmit替换Omit的所有实例,我当然也不会建议TypeScript的库更改Omit的定义,因为这会影响其他人。如相关GitHub问题评论(microsoft/TypeScript#31501(中所述:

Omit的所有可能定义都有一定的权衡;我们已经选择了一个我们认为最适合的,在这一点上,如果不引起一系列难以确定的突破,就无法真正改变它。

代码的游乐场链接

相关内容

  • 没有找到相关文章

最新更新