这里的主要问题是,当您为一个像MY_OBJECT
这样的变量加上一个像MyObjectType
这样的(非union)类型时,类型判断器所知道的关于它的类型就这么多了.它不会记住有关变量初始值设定项的任何更具体的信息;这样的知识将永远丢失.所以typeof MY_OBJECT
只是MyObjectType
,keyof typeof MY_OBJECT
只是string
.
而不是注释,您真的只希望将MY_OBJECT
‘S初始化器是有效的MyObjectType
,而不是将其完全扩大到MyObjectType
.这看起来像是一份the satisfies
operator人的工作.如果你写的是const vbl = expr satisfies Type
而不是const vbl: Type = expr
,你通常会得到更令人满意的行为:
const MY_OBJECT = {
BANANA: {
a: "a"
},
LEMON: {
a: "A"
}
} satisfies MyObjectType // <-- do this instead
如果初始值设定项不满足MyObjectType
,则仍会发出警告:
const MY_OBJECT = {
BANANA: {
a: "a"
},
LEMON: {
a: 1 // error!
//~ <-- Type 'number' is not assignable to type 'string'.
}
} satisfies MyObjectType;
现在MY_OBJECT
的类型比MyObjectType
更具体,你可以通过智能感知观察到:
/* const MY_OBJECT: {
BANANA: { a: string; };
LEMON: { a: string; };
} */
你可以根据需要获得它的密钥:
type MyObjectKey = keyof typeof MY_OBJECT;
// type MyObjectKey = "BANANA" | "LEMON"
现在,您可以相应地定义MyType
.您不能为此使用index signature;索引签名只支持像string
这样的"宽"类型.您有一组已知的密钥.你应该用mapped type代替.由于您希望这些属性是可选的,因此可以使用?
mapping modifier:
type MyType = { [K in MyObjectKey]?: string | number };
/* type MyType = {
BANANA?: string | number | undefined;
LEMON?: string | number | undefined;
} */
现在你会得到你想要的行为:
const newObject: MyType = {
BANANA: "banana",
APPLE: "apple" // <-- error
};
不过,最后要提的是.仅当您在需要没有该属性的类型的位置直接使用object literal时,才会发生类似上面APPLE
的错误.它们主要是Linter警告,您可能会因为使用了编译器会立即忘记的键而犯了错误.它们并不是一个类型错误,您可以通过分多个步骤进行赋值来避免检测,如下所示:
const obj = { BANANA: "banana", APPLE: "apple" };
const newObject2: MyType = obj; // this will never be an error.
根据编译器的说法,这很好,因为obj
知道大约APPLE
.这有点超出了本文的范围,因为一切都如您所愿地工作.但无论如何,你都应该意识到这一点.有关更多信息,请参见Typescript: prevent assignment of object with more properties than is specified in target interface或Why are excess properties allowed in Typescript when all properties in an interface are optional?或typescript: validate excess keys on value, returned from function或Unexpected behaviour of Typescript Record<number, Type> type,或任何其他提及"超额财产"和"Typescript "的问题.
Playground link to code个