【问题标题】:How exactly do recursive conditional types work?递归条件类型究竟是如何工作的?
【发布时间】:2021-07-05 21:28:13
【问题描述】:
  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;
    };
}

我错过了什么?

【问题讨论】:

  • 看起来是同一类型;只是显示不同。当我有机会时,我会验证。有一些方法可以说服编译器扩展类型……因为我在移动设备上,所以现在还没有真正准备好回答。
  • 将字符串分配给 address.street 确实有效

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


【解决方案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&lt;Person&gt;,它会向下遍历 Person 并显式写出每个属性。假设 DiplomacyError 是这个(因为在问题中缺少 minimal reproducible example):

interface DiplomacyError {
    whatIsADiplomacyError: string;
}

那么ExpandRecurively&lt;Person&gt;就是:

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中所述。单个大写字母优先于整个单词,以便将它们与特定类型区分开来。因此,我将Property替换为您的示例使用K 表示“键”。这可能看起来自相矛盾,但由于这种约定,TypeScript 开发人员更有可能立即将K 理解为比Property 的通用属性键。你是,当然,可以继续使用Property 或任何你喜欢的东西;毕竟这只是一个约定,而不是某种诫命。但我只是想指出约定存在。)

Playground link to code

【讨论】:

  • 我仍然很难理解为什么我需要“手动”推断 O 类型,但它确实有效...... DiplomacyError 是一个到字符串的枚举映射,它似乎没有为了保留类型别名,编译器似乎在做string | DiplomacyError -&gt; string | string -&gt; string(非常感谢。)
  • 但这将是一个完全不同的问题:)
猜你喜欢
  • 2013-03-09
  • 2013-07-06
  • 2021-01-27
  • 2015-02-20
  • 2014-01-20
  • 2019-01-05
  • 1970-01-01
  • 1970-01-01
  • 2021-07-03
相关资源
最近更新 更多