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

时间:2021-04-10 03:46:14

标签: typescript typescript-generics conditional-types nested-generics

  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;
    };
}

我错过了什么?

1 个答案:

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

Playground link to code

相关问题