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

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 或任何你喜欢的东西;毕竟这只是一个约定,而不是某种戒律。但是我只是想指出约定存在。)

Playground link to code