递归条件类型究竟是如何工作的? [英] How exactly do recursive conditional types work?

查看:25
本文介绍了递归条件类型究竟是如何工作的?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

  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".这可能看起来很矛盾,但是由于这种约定,KProperty 更容易被 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆