映射类型很好,因为它们允许对象结构的灵活性,这是JavaScript所熟知的。但它们对类型系统有一些关键的影响。以这个例子为例。
type Messages =
'CHANNEL_OPEN' | 'CHANNEL_CLOSE' | 'CHANNEL_FAIL' |
'MESSAGE_CHANNEL_OPEN' | 'MESSAGE_CHANNEL_CLOSE' |
'MESSAGE_CHANNEL_FAIL'
type ChannelDefinition = {
[key: string]: {
open: Messages,
close: Messages,
fail: Messages
}
}
这是一个通用的消息传递库,它需要一个 "通道定义",其中可以定义多个通道令牌。这个通道定义对象的键是用户希望它是什么。所以这是一个有效的通道定义。
const impl: ChannelDefinition = {
test: {
open: 'CHANNEL_OPEN',
close: 'CHANNEL_CLOSE',
fail: 'CHANNEL_FAIL'
},
message: {
open: 'MESSAGE_CHANNEL_OPEN',
close: 'MESSAGE_CHANNEL_CLOSE',
fail: 'MESSAGE_CHANNEL_FAIL'
}
}
当我们想如此灵活地访问我们定义的键时,我们有一个问题。比方说,我们有一个函数,可以打开一个通道。我们传递整个通道定义对象,以及我们想要打开的通道。
declare function openChannel(
def: ChannelDefinition,
channel: keyof ChannelDefinition
)
那么,ChannelDefinition 的键是什么?嗯,是每个键:[key: string] 。因此,当我们指定一个特定的类型时,TypeScript将impl 作为这个特定的类型,忽略了实际的实现。契约被履行了。继续前进。这允许错误的键被传递。
// Passes, even though "massages" is no part of impl
openChannel(impl, 'massages')
所以我们更关心的是实际的实现,而不是我们分配给常量的类型。这意味着我们必须摆脱ChannelDefinition 类型,并确保我们关心对象的实际类型。
首先,openChannel 函数应该接受任何属于ChannelDefinition 的子类型的对象,但要与具体的子类型一起工作。
- declare function openChannel(
- def: ChannelDefinition,
- channel: keyof ChannelDefinition
- )
+ declare function openChannel<T extends ChannelDefinition>(
+ def: T,
+ channel: keyof T
+ )
TypeScript现在在两个层面上工作。
- 检查
T是否实际扩展了ChannelDefinition。如果是,我们就用类型来工作T - 我们所有的函数参数都是用通用的
T。这也意味着我们通过keyof T,获得了T的真实键。
为了从中受益,我们必须去掉impl 的类型定义。明确的类型定义覆盖了所有的实际类型。从我们明确指定类型的那一刻起,TypeScript将其视为ChannelDefinition ,而不是实际的底层子类型。我们还必须设置const 上下文,所以我们可以将所有字符串转换为它们的单位类型(从而符合Messages )。
- const impl: ChannelDefinition = { ... };
+ const impl: { ... } as const;
没有const 上下文,impl的推断类型是。
/// typeof impl
{
test: {
open: string;
close: string;
fail: string;
};
message: {
open: string;
close: string;
fail: string;
};
}
有了const 上下文,impl 的实际类型就是现在。
/// typeof impl
{
test: {
readonly open: "CHANNEL_OPEN";
readonly close: "CHANNEL_CLOSE";
readonly fail: "CHANNEL_FAIL";
};
message: {
readonly open: "MESSAGE_CHANNEL_OPEN";
readonly close: "MESSAGE_CHANNEL_CLOSE";
readonly fail: "MESSAGE_CHANNEL_FAIL";
};
}
const 上下文使我们能够满足 所制定的契约。现在, 正确的错误。ChannelDefinition openChannel
openChannel(impl, 'messages') // ✅ satisfies contract
openChannel(impl, 'massages') // 💥 bombs
你可能会在一个空间里需要使用具体的类型,满足ChannelDefinition 的契约,在一个函数之外。为此,我们可以使用Validate<T, U> 帮助类型来模仿同样的行为。
type Validate<T, U> = T extends U ? T : never;
使用方法如下。
const correctImpl = {
test: {
open: 'CHANNEL_OPEN', close: 'CHANNEL_CLOSE', fail: 'CHANNEL_FAIL'
}
} as const;
const wrongImpl = {
test: {
open: 'OPEN_CHANNEL', close: 'CHANNEL_CLOSE', fail: 'CHANNEL_FAIL'
}
} as const;
// ✅ returns typeof correctImpl
type ValidatedCorrect
= Validate<typeof correctImpl, ChannelDefinition>;
// 💥 returns never
type ValidatedWrong
= Validate<typeof wrongImpl, ChannelDefinition>;