TypeScript 中目前没有与您想要的 SomeShape 类型相对应的特定类型。 Lowercase<string> 仅计算为 string;即使这不是真的,pattern template literal types 像 `${string}Required` 目前也不能用作对象的键类型;请参阅microsoft/TypeScript#42192 了解更多信息。
相反,您可以将SomeShape 表示为generic 类型,作为候选类型的约束。也就是说,你创建一个类似ValidSomeShape<T> 的类型,这样T extends ValidSomeShape<T> 当且仅当T 是一个有效的SomeShape。它可能看起来像这样:
type ValidSomeShape<T extends object> = { [K in keyof T as
K extends `${infer P}Required` ? `${Lowercase<P>}Required` :
`${Lowercase<Extract<K, string>>}Required`]:
boolean
} extends infer O ? {[K in keyof O]: O[K]} : never;
这个工作的方式是:编译器re-maps the keys 的T 到那些是有效的;如果键 K 不以 "Required" 结尾,那么我们追加它。否则,我们将"Required" 之前的部分转换为自身的小写版本。我们确保属性类型是boolean。
以extends infer O ? ... 结尾的部分是trick from the answer to another question,它鼓励编译器在IntelliSense 中列出ValidSomeShape<T> 的实际属性,而不是显示相当不透明的ValidSomeShape<T> 名称。您宁愿在错误消息中看到 {fooRequired: boolean} 而不是 ValidSomeShape<{foo: string}>。
继续:为了避免人们不得不手动指定T,您可以创建一个通用辅助函数asSomeShape(),从其输入推断T:
const asSomeShape = <T extends ValidSomeShape<T>>(obj: T) => obj;
所以不要注释const state: SomeShape = {...},而是写const state = asSomeShape({...})。
让我们试试吧:
const state = asSomeShape({
usersRequired: false,
ordersRequired: false,
booksRequired: false,
}); // okay
这编译没有错误。但是请注意当您做错事时会发生什么:
const badState1 = asSomeShape({
usersRequired: false,
ordersRequired: 123, // error!
//~~~~~~~~~~~~~~ <-- // Type 'number' is not assignable to type 'boolean'
booksRequired: false,
}); // okay
const badState2 = asSomeShape({
usersRequired: false,
ordersRequired: false,
BooksRequired: false, // error!
//~~~~~~~~~~~~~~~~~~~~
// Object literal may only specify known properties, but 'BooksRequired' does not exist in type
// '{ usersRequired: boolean; ordersRequired: boolean; booksRequired: boolean; }'.
// Did you mean to write 'booksRequired'?
}); // okay
const badState3 = asSomeShape({
users: false, // error!
//~~~~~~~~~~~~
// Object literal may only specify known properties, and 'users' does not exist in type
// '{ usersRequired: boolean; ordersRequired: boolean; booksRequired: boolean; }'
ordersRequired: false,
booksRequired: false,
}); // okay
您可以看到每次失败都会导致有用的错误消息。 ordersRequired 属性是 number 而不是预期的 boolean; BooksRequired 属性可能应该是 booksRequired;并且users 属性也是错误的(编译器似乎认为它与usersRequired 不够接近,暗示您应该改写它,但它确实说它希望在那里看到usersRequired)。
所以,至少在 TypeScript 4.2 中,这已经差不多了。
由于使用通用约束比特定类型更复杂,您可能只想在与尚未验证的对象交互的函数中使用ValidSomeShape<T>...例如某些面向外部的端点图书馆。验证对象后,您可以将其类型扩展为不太精确但非泛型类型,如 Record<string, boolean> 或其他类型,并将其作为更广泛的类型在库中传递:
export function userFacingLibraryFunction<T extends ValidSomeShape<T>>(someShape: T): void {
// now that someShape has been validated, we can pass it to our internal functions:
internalLibraryFunction(someShape);
}
// not exported
function internalLibraryFunction(alreadyValidatedSomeShape: Record<string, boolean>): void {
Object.keys(alreadyValidatedSomeShape).filter(k => alreadyValidatedSomeShape[k]);
}
Playground link to code