export type Parser = NumberParser | StringParser;
type NumberParser = (input: string) => number | DiplomacyError;
type StringParser = (input: string) => string | DiplomacyError;
export interface Schema {
[key: string]: Parser | Schema;
}
export type RawType<T extends Schema> = {
[Property in keyof T]: T[Property] extends Schema
? RawType<T[Property]>
: ReturnType<Exclude<T[Property], Schema>>;
};
// PersonSchema is compliant the Schema interface, as well as the address property
const PersonSchema = {
age: DT.Integer(DT.isNonNegative),
address: {
street: DT.String(),
},
};
type Person = DT.RawType<typeof PersonSchema>;
遗憾的是 type Person
被推断为:
type Person = {
age: number | DT.DiplomacyError;
address: DT.RawType<{
street: StringParser;
}>;
}
相反,我希望得到:
type Person = {
age: number | DT.DiplomacyError;
address: {
street: string | DT.DiplomacyError;
};
}
我错过了什么?
答案 0 :(得分:1)
显示的 Person
与您预期的类型之间的差异几乎只是装饰性的。编译器在评估和显示类型时遵循一组启发式规则。这些规则会随着时间的推移而发生变化,偶尔也会有所调整,例如 TypeScript 4.2 中引入的 "smarter type alias preservation" 支持。
查看类型或多或少等效的一种方法是创建它们:
type Person = RawType<PersonSchema>;
/*type Person = {
age: number | DiplomacyError;
address: RawType<{
street: StringParser;
}>;
}*/
type DesiredPerson = {
age: number | DiplomacyError;
address: {
street: string | DiplomacyError;
};
}
然后看到编译器认为它们相互可分配:
declare let p: Person;
let d: DesiredPerson = p; // okay
p = d; // okay
这些行没有导致警告的事实意味着,根据编译器,任何 Person
类型的值也是 DesiredPerson
类型的值,反之亦然。
所以也许这对你来说已经足够了。
如果您真的关心类型的表示方式,您可以使用this answer中描述的技术:
// expands object types recursively
type ExpandRecursively<T> = T extends object
? T extends infer O ? { [K in keyof O]: ExpandRecursively<O[K]> } : never
: T;
如果我计算 ExpandRecursively<Person>
,它会遍历 Person
并明确写出每个属性。假设 DiplomacyError
是这样的(因为在问题中缺少 minimal reproducible example):
interface DiplomacyError {
whatIsADiplomacyError: string;
}
那么 ExpandRecurively<Person>
是:
type ExpandedPerson = ExpandRecursively<Person>;
/* type ExpandedPerson = {
age: number | {
whatIsADiplomacyError: string;
};
address: {
street: string | {
whatIsADiplomacyError: string;
};
};
} */
哪个更接近您想要的。实际上,您可以重写 RawType
以使用此技术,例如:
type ExpandedRawType<T extends Schema> = T extends infer O ? {
[K in keyof O]: O[K] extends Schema
? ExpandedRawType<O[K]>
: O[K] extends (...args: any) => infer R ? R : never;
} : never;
type Person = ExpandedRawType<PersonSchema>
/* type Person = {
age: number | DiplomacyError;
address: {
street: string | DiplomacyError;
};
} */
这正是您想要的形式。
(旁注:类型参数有一个命名约定,如this answer中提到的。单个大写字母优先于整个单词,以便将它们与特定类型区分开来。因此,我将{{ 1}} 在您的示例中使用 Property
表示“key”。这可能看起来很矛盾,但由于这种约定,K
更有可能立即被 TypeScript 开发人员理解为通用属性键,而不是K
是。当然,您可以继续使用 Property
或任何其他您喜欢的东西;毕竟这只是惯例,而不是某种戒律。但我只是想指出约定存在。)