递归条件类型究竟是如何工作的?
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
被推断为:
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 中引入的 "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
类型的值,反之亦然。
也许这对你来说就足够了。
如果您真的关心类型的表示方式,可以使用 :
中描述的技术
// 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;
};
} */
这正是您想要的形式。
(旁注:类型参数有一个命名约定,如中所述。单个大写字母优先于整个单词,以便将它们与特定类型区分开来。因此,我已经替换Property
在您的示例中,K
代表“键”。这看起来似乎自相矛盾,但由于这种约定,TypeScript 开发人员更有可能立即将 K
理解为通用 属性 比 Property
更重要。当然,你可以自由地继续使用 Property
或任何你喜欢的东西;毕竟这只是一个约定,而不是某种戒律。但是我只是想指出约定存在。)
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;
};
}
我错过了什么?
显示的 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
类型的值,反之亦然。
也许这对你来说就足够了。
如果您真的关心类型的表示方式,可以使用
// 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;
};
} */
这正是您想要的形式。
(旁注:类型参数有一个命名约定,如Property
在您的示例中,K
代表“键”。这看起来似乎自相矛盾,但由于这种约定,TypeScript 开发人员更有可能立即将 K
理解为通用 属性 比 Property
更重要。当然,你可以自由地继续使用 Property
或任何你喜欢的东西;毕竟这只是一个约定,而不是某种戒律。但是我只是想指出约定存在。)