定义基于递归泛型接口的 TypeScript 类

Don*_*rdy 5 generics typescript typescript-generics

我正在为一个项目设置一个基础数据结构,希望有一个抽象的基础GraphNode对象,许多其他对象将从中继承。每个 GraphNode 子类都具有元数据,其中可能包括文字(字符串、数字等)和对其他 GraphNode 派生类型的引用。

\n

子类示例:

\n
interface IPerson {\n  name: string;\n  age: number;\n  friend: Person;\n  pet: Pet;\n}\n\ninterface IPet {\n  type: \'dog\' | \'cat\';\n  name: string;\n}\n\nclass Person extends GraphNode<IPerson> {}\nclass Pet extends GraphNode<IPet> {}\n
Run Code Online (Sandbox Code Playgroud)\n

用法示例:

\n
const spot = new Pet()\n  .set(\'type\', \'dog\')\n  .set(\'name\', \'Spot\');\n\nconst jo = new Person()\n  .set(\'name\', \'Jo\')\n  .set(\'age\', 41)\n  .set(\'pet\', spot) // \xe2\x9a\xa0\xef\xb8\x8f Should not accept GraphNode argument.\n  .setRef(\'pet\', spot); // \xe2\x9c\x85 Correct.\n\nconst sam = new Person()\n  .set(\'name\', \'Sam\')\n  .set(\'age\', 45)\n  .set(\'friend\', jo) // \xe2\x9a\xa0\xef\xb8\x8f Should not accept GraphNode argument.\n  .setRef(\'friend\', jo); // \xe2\x9c\x85 Correct.\n
Run Code Online (Sandbox Code Playgroud)\n

我的问题是 \xe2\x80\x94 当它的泛型类型递归地依赖于 GraphNode 类时,如何定义 GraphNode 基类?这是我的尝试:

\n

实用程序类型:

\n
type Literal = null | boolean | number | string;\n\ntype LiteralAttributes<Base> = {\n    [Key in keyof Base]: Base[Key] extends Literal ? Base[Key] : never;\n};\n\n// \xe2\x9a\xa0\xef\xb8\x8f ERROR \xe2\x80\x94 Generic type \'GraphNode<Attributes>\' requires 1 type argument(s).\ntype RefAttributes<Base> = {\n    [Key in keyof Base]: Base[Key] extends infer Child\n        ? Child extends GraphNode | null\n            ? Child | null\n            : never\n        : never;\n};\n
Run Code Online (Sandbox Code Playgroud)\n

抽象图定义:

\n
type GraphAttributes = {[key: string]: Literal | GraphNode | null};\n\nabstract class GraphNode<Attributes extends GraphAttributes> {\n  public attributes = {} as LiteralAttributes<Attributes> | RefAttributes<Attributes>;\n\n  public get<K extends keyof LiteralAttributes<Attributes>>(key: K): Attributes[K] {\n    return this.attributes[key]; // \xe2\x9a\xa0\xef\xb8\x8f Missing type information.\n  }\n\n  public set<K extends keyof LiteralAttributes<Attributes>>(key: K, value: Attributes[K]): this {\n    this.attributes[key] = value;\n    return this;\n  }\n\n  public getRef<K extends keyof RefAttributes<Attributes>>(key: K): Attributes[K] | null {\n    return this.attributes[key]; // \xe2\x9a\xa0\xef\xb8\x8f Missing type information.\n  }\n\n  public setRef<K extends keyof RefAttributes<Attributes>>(key: K, value: Attributes[K] | null): this {\n    this.attributes[key] = value; // \xe2\x9a\xa0\xef\xb8\x8f Missing type information.\n    return this;\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这无法编译,我已在上面的注释中标记了 TS 错误。我认为这归结为一个基本问题 \xe2\x80\x93 我想稍后使用引用泛型类的类型参数来扩展泛型类,但我不知道该怎么做。我可以忍受 GraphNode 类本身没有严格的类型检查,但我确实希望它的子类有严格的类型检查。

\n

我还创建了一个示例 TypeScript Playground来重现此带有编译器错误的代码。

\n

jca*_*alz 5

原始代码中存在很多问题,导致其行为不符合您想要的方式。T首先,要获取其值可分配给 type 的对象类型的键V,您可以执行以下操作:

type KeysMatching<T, V> = {[K in keyof T]-?: T[K] extends V ? K : never}[keyof T];
Run Code Online (Sandbox Code Playgroud)

这会将属性 from映射T到键K或 ,never 具体取决于这些属性是否可分配给V,然后立即索引到该映射类型keyof T以获取我们关心的键的并集。

我们可以专门获取T其属性可分配给其属性的键Literal以及其属性可分配给某种GraphNode<X>类型的键:

type LiteralKeys<T> = { [K in keyof T]-?: T[K] extends Literal ? K : never }[keyof T];
type RefKeys<T> = { [K in keyof T]-?: T[K] extends GraphNode<any> ? K : never }[keyof T]
Run Code Online (Sandbox Code Playgroud)

因此,我们可以使用whichLiteralKeys<A>来代替你的keyof LiteralAttributes<A>which 不起作用,因为keyof LiteralAttributes<A>它始终与keyof A(你将错误的属性值映射到never,但这并没有消除该键)相同。


现在GraphNode可以这样定义:

abstract class GraphNode<A extends Record<keyof A, Literal | GraphNode<any>>> {
  public attributes: Partial<A> = {};

  public get<K extends LiteralKeys<A>>(key: K): A[K] | undefined {
    return this.attributes[key];
  }

  public set<K extends LiteralKeys<A>>(key: K, value: A[K]): this {
    this.attributes[key] = value;
    return this;
  }

  public getRef<K extends RefKeys<A>>(key: K): A[K] | undefined {
    return this.attributes[key];
  }

  public setRef<K extends RefKeys<A>>(key: K, value: A[K] | undefined): this {
    this.attributes[key] = value;
    return this;
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,您在类型参数中GraphNode<A>泛型的A,并且无论何时引用GraphNode都必须指定泛型类型参数。你不能只是说GraphNode。如果您不知道或不关心类型参数应该是什么,您可以使用type any例如GraphNode<any>。这总是有效的,但最终可能会允许一些你不想允许的事情。在这种情况下,可能没问题。

A限制Record<keyof A, Literal | GraphNode<any>>而不是{[k: string]: Literal | GraphNode<any>}因为我们真的不想要求A有一个string 索引签名。通过限制A我们Record<keyof A, ...>是说键可以是任何它们碰巧是什么。

attributes属性的类型为Partial<A>。这是A因为我们想要持有非常相似的东西A,而不是类似的东西LiteralAttributes<A> | RefAttributes<A>。我们使用Partial<T>实用程序类型A来确认在任何给定时间它可能不具有set的所有属性;事实上它被初始化为{},它根本没有属性。这也意味着我已经更改了get()getRef()返回类型以包含undefined.

和方法在 中通用get(),而和方法在 中通用。对于大多数合理的类型,和是互斥的,并且一起组成 的全部。如果有任何属性本身是 的并集或交集,那么这可能不是真的。我在这里并不担心这一点,但如果确实发生这种情况,您可能会遇到一些奇怪的边缘情况。set()K extends LiteralKeys<A>getRef()setRef()K extends RefKeys<A>ALiteralKeys<A>RefKeys<A>keyof TALiteralGraphNode<any>


让我们确保它的行为符合您的要求。您的子类和用法按预期编译,甚至导致以下结果:

const jo = new Person()
  .set('name', 'Jo')
  .set('age', 41)
  .set('pet', spot) // error! Argument of type '"pet"' 
  // is not assignable to parameter of type 'LiteralKeys<IPerson>'
  .setRef('pet', spot); 

const sam = new Person()
  .set('name', 'Sam')
  .set('age', 45)
  .set('friend', jo) // error! Argument of type '"friend"'
  // is not assignable to parameter of type 'LiteralKeys<IPerson>'
  .setRef('friend', jo); 
Run Code Online (Sandbox Code Playgroud)

所以看起来不错!

Playground 代码链接