递归条件类型究竟是如何工作的


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中引入的支持。

一种方法可以看出这两种类型或多或少是等效的,那就是同时创建它们:

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是这样的(因为问题中缺少最小的可重复示例(:

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,表示"key"。这可能看起来很矛盾,但正是因为这个约定,K更有可能立即被type理解编写脚本让开发人员成为比Property更通用的属性键。当然,你可以继续使用Property或任何你喜欢的东西;毕竟,这只是一个惯例,而不是某种戒律。但我只是想指出,公约是存在的。(

游乐场链接到代码

最新更新