递归条件类型究竟是如何工作的? [英] How exactly do recursive conditional types work?
问题描述
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
被推断为:
Sadly type Person
is inferred as:
type Person = {
age: number | DT.DiplomacyError;
address: DT.RawType<{
street: StringParser;
}>;
}
相反,我希望得到:
type Person = {
age: number | DT.DiplomacyError;
address: {
street: string | DT.DiplomacyError;
};
}
我错过了什么?
推荐答案
显示的 Person
与您期望的类型之间的差异几乎只是表面上的.编译器在评估和显示类型时遵循一组启发式规则.这些规则随着时间的推移而改变,偶尔会进行调整,例如 更智能的类型别名保留" 支持在 TypeScript 4.2 中引入.
The difference between the Person
displayed and the type you expected is pretty much just cosmetic. The compiler has a set of heuristic rules it follows when evaluating and displaying types. These rules have changed over time and are occasionally tweaked, such as the "smarter type alias preservation" support introduced in TypeScript 4.2.
查看类型或多或少等效的一种方法是创建它们:
One way to see that the types are more or less equivalent is to create both of them:
type Person = RawType<PersonSchema>;
/*type Person = {
age: number | DiplomacyError;
address: RawType<{
street: StringParser;
}>;
}*/
type DesiredPerson = {
age: number | DiplomacyError;
address: {
street: string | DiplomacyError;
};
}
然后看到编译器认为它们相互可分配:
And then see that the compiler considers them mutually assignable:
declare let p: Person;
let d: DesiredPerson = p; // okay
p = d; // okay
这些行没有导致警告的事实意味着,根据编译器,Person
类型的任何值也是 DesiredPerson
类型的值,并且反之亦然.
The fact that those lines did not result in a warning means that, according to the compiler, any value of type Person
is also a value of type DesiredPerson
, and vice versa.
所以也许这对你来说已经足够了.
So maybe that's enough for you.
如果您真的关心类型的表示方式,您可以使用这个答案中描述的技术:
If you really care about how the type is represented, you can use techniques such as described in 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
并明确写出每个属性.假设 DiplomacyError
是这样的(因为在问题中需要 最小可重现示例):
If I compute ExpandRecursively<Person>
, it walks down through Person
and explicitly writes out each of the properties. Assuming that DiplomacyError
is this (for want of a minimal reproducible example in the question):
interface DiplomacyError {
whatIsADiplomacyError: string;
}
那么ExpandRecurively
是:
type ExpandedPerson = ExpandRecursively<Person>;
/* type ExpandedPerson = {
age: number | {
whatIsADiplomacyError: string;
};
address: {
street: string | {
whatIsADiplomacyError: string;
};
};
} */
哪个更接近您想要的.实际上,您可以重写 RawType
以使用此技术,例如:
which is closer to what you want. In fact, you could rewrite RawType
to use this technique, like:
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;
};
} */
这正是您想要的形式.
(旁注:类型参数有一个命名约定,如这个答案中所述.单个大写字母是优先于整个单词,以便将它们与特定类型区分开来.因此,我将您示例中的 Property
替换为 K
表示key".这可能看起来很矛盾,但是由于这种约定,K
比 Property
更容易被 TypeScript 开发人员立即理解为通用属性键.当然,您可以自由继续使用 Property
或其他任何你喜欢的东西;毕竟这只是一个约定,而不是某种戒律.但我只是想指出约定存在.)
(Side note: there is a naming convention for type parameters, as mentioned in this answer. Single capital letters are preferred over whole words, so as to distinguish them from specific types. Hence, I have replaced Property
in your examples with K
for "key". It might seem paradoxical, but because of this convention, K
is more likely to be immediately understood by TypeScript developers to be a generic property key than Property
is. You are, of course, free to continue using Property
or anything else you like; it's just a convention, after all, and not some sort of commandment. But I just wanted to point out that the convention exists.)
这篇关于递归条件类型究竟是如何工作的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!