我遇到了一个问题,我不知道这是实际的问题还是我这边的误解。
考虑到这段代码
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
不应允许obj
将toto
和tata
作为字符串 - 如果
{ foo: string; bar: string }
可分配给Props
,则keyof Props
应仍然具有foo
和bar
的已知属性。(否则foo
和bar
在我看来应该是未知的(
看起来不兼容,但我发现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
将每个密钥重新映射到never
或P
。如果您使用它而不是Omit
,则会出现所需的类型:
type PropsWithoutFoo = AlternativeOmit<Props, "foo">;
/* type PropsWithoutFoo = {
[x: string]: unknown;
bar: string;
} */
由于AlternativeOmit
和Omit
具有不同的行为,因此很可能存在边缘情况,其中Omit
的行为符合您的要求,而AlternativeOmit
则不然。所以我不会说你应该用AlternativeOmit
替换Omit
的所有实例,我当然也不会建议TypeScript的库更改Omit
的定义,因为这会影响其他人。如相关GitHub问题评论(microsoft/TypeScript#31501(中所述:
Omit
的所有可能定义都有一定的权衡;我们已经选择了一个我们认为最适合的,在这一点上,如果不引起一系列难以确定的突破,就无法真正改变它。
代码的游乐场链接