首先让我说我讨厌这种结构。如果有办法将代码重写为更合乎逻辑的Team 类型,那就去做吧。但是能做到吗?是的,它可以。 Team 必须是 type 而不是接口,因为接口只能有已知的键。
让我们先把它分成几块。
TeamBase 定义了始终存在的属性:
interface TeamBase {
startDate: Timestamp
endDate: Timestamp
agenda: Array<{
date: Date | Timestamp
title: string
description: string
}>
}
UidProperties 定义了未知键的内容:
interface UidProperties {
mail: string
name: string
photoURL: string
}
这是一个实用类型,可用于使用映射类型根据键的类型声明属性:
type HasProperty<Key extends keyof any, Value> = {
[K in Key]: Value;
}
如果通用 Key 是字符串文字,那么 K in Key 的唯一可能值就是那个确切的字符串,所以 HasProperty<"myKey", string> 解析为 { myKey: string; }
所以现在我们需要定义类型Team。我们知道它需要包含TeamBase 的所有属性,并且我们知道有一些字符串键的值为UidProperties。之前给您带来问题的一个重要事实是此键不能是 TeamBase 的键之一。所以我们不使用string,而是使用Exclude<string, keyof TeamBase>。
您可以使用HasProperty<...> 表示每个 不是TeamBase 键的字符串键的值为UidProperties,但我认为最好使用Partial<HasProperty<...>>,它说这些未知的字符串键中的任何一个都可以是UidProperties,但也可以是undefined。当您访问一个未知的密钥时,您可能需要再次检查该值。
所以我们最终的 Team 类型是:
export type Team = TeamBase & Partial<HasProperty<Exclude<string, keyof TeamBase>, UidProperties>>
这按预期工作。因为我们已经考虑到uid 未定义的可能性,所以如果不通过? 的可选链接或if 语句等排除undefined,就不能访问uid 的属性。
function f1(team: Team) {
const startDate: Timestamp = team.startDate;
const uid: UidProperties | undefined = team['randomKey']
const name = uid?.name;
if ( uid !== undefined ) {
const mail = uid.mail;
}
}