我试图将对象的所有属性推断为文字。
我很清楚我可以使用
const
断言。
但是,我不希望结果类型具有
readonly
属性。
问题是这样的:
type Options = {
min?: number;
};
class E1<O extends Options> {
constructor(options: O) {}
}
const e1 = new E1({ min: 1 });
e1
的类型是:
E1<{
min: number; // Oh no! This should be `1`
}>
现在,我知道我可以使用
const
类型参数,如下所述:TypeScript 5.0 Docs。
让我们尝试一下:
class E2<const O extends Options> {
constructor(options: O) {}
}
const e2 = new E2({ min: 1 });
e2
的类型是:
E2<{
readonly min: 1; // Now it's `1`. Great! But it's `readonly`?
}>
我想要的结果是这样的:
typeof
eN
:(eN
=“示例编号 N”)
EN<{
min: 1;
}>
现在我尝试了几种解决方案,但都不起作用:
class E3<const C extends Options> {
constructor(c: { -readonly [K in keyof C]: C[K] }) {}
}
const e3 = new E3({ min: 1 });
类型
e3
:
E3<{
readonly min: 1;
}>
或者这个:
type NoRead<T> = {
-readonly [K in keyof T]: T[K];
};
class E4<const C extends NoRead<Options>> {
constructor(c: C) {}
}
const e4 = new E4({ min: 1 });
类型
e4
:
E4<{
readonly min: 1;
}>
对此有一个明显的解决方案:
class E5<C extends Options> {
constructor(c: C) {}
}
const e5 = new E5({ min: 1 as const });
类型
e5
:
E5<{
min: 1;
}>
但是,随着配置对象大小的增长,const 断言变得不合理。
现在,我已经可以看到这个问题:“你为什么想要那个?”
答案是,它主要用于 IntelliSense 目的,以减少此类更复杂结构中的视觉负载。
我希望这是足够的理由。
摘要:“我想将泛型的值推断为文字,同时避免使用
readonly
修饰符。”
这里是 Playground 链接:TS Playground。
您可以通过使用“反向映射类型”来实现此目的(请参阅 microsoft/TypeScript#53018 上的评论),也称为“从映射类型推断”(如已弃用的 TS 手册中记录的 ,但由于某种原因而不是这样)在当前的版本中,尽管这一点没有任何改变)。
也就是说,如果您有 function foo<T>(ft: F<T>): void
形式的
generic调用或构造签名,那么 TypeScript 有时可以从
映射类型
T
的值推断出类型参数 F<T>
。 只有当映射类型 F<T>
是 T
中的 同态(“同态映射类型”是什么意思?)时,这才有可能。这是一个“反向”映射类型,因为如果您调用
foo(fa)
,TypeScript 必须反向运行映射类型,从输出 T
获取输入 F<T>
。
对于您的示例代码,它看起来像这样:
class E<const C extends Options> {
constructor(c: Readonly<C>) { }
}
const
类型参数 C
来鼓励文字类型的推断。但我们说构造函数参数 c
的类型是 Readonly<C>
,使用 Readonly
实用程序类型(实现为同态映射类型 type Readonly<T> = { readonly [P in keyof T]: T[P]; }
)。
它的意思是这样的:当你调用
new E(c)
时,TypeScript 会将 c
的类型视为 Readonly<C>
。为了从C
的类型推断c
,需要反转Readonly<>
的操作。这意味着 C
在概念上是 typeof c
的 non只读版本:
const e = new E({ min: 1, str: "abc" });
/* const e: E<{ min: 1; str: "abc"; }> */
显然这有效。
const
类型参数的行为就像您调用 new E({min: 1, str: "abc"} as const
一样,这会推断 c
属于 {readonly min: 1, readonly str: "abc"}
类型,并且由于它是 Readonly<C>
,TypeScript 将 C
与 {min: 1, str: "abc"}
进行匹配。实际上,这对我来说有点令人惊讶,因为可以说 C
仍然是 {readonly min: 1, readonly str: "abc"}
是正确的;因为无论哪种方式 Readonly<C>
都是相同的。但幸运的是,TypeScript 决定实际应用 -readonly
映射修饰符。
在反向映射类型不能神奇地工作的情况下,您需要自己做。也就是说,如果
new <C extends Options>(c: Readonly<C>) => E<C>
没有这样做,那么 new <C extends Options>(c: C) => E<Mutable<C>>
应该这样做,其中 type Mutable<T> = { -readonly [K in keyof T]: T[K] }>
。不幸的是,TypeScript 永远不会为 class
声明推断出这样的事情,这意味着您需要自己手动编写它并为其分配一个构造函数:
class _E<C extends Options> {
constructor(c: C) { }
}
type E<C extends Options> = _E<C>;
const E: new <const C extends Options>(
e: C) => E<{ -readonly [K in keyof C]: C[K] }> = _E;
这给出了相同的结果:
const e = new E({ min: 1, str: "abc" });
/* const e: E<{ min: 1; str: "abc"; }> */
但这更复杂,因此反向映射类型在工作时更可取。