I'm setting up a base datastructure for a project, hoping to have an abstract GraphNode base object from which many other objects will inherit. Each GraphNode subclass has metadata that could include both literals (string, numbers, ...) and references to other GraphNode-derived types.
Example subclasses:
interface IPerson {
name: string;
age: number;
friend: Person;
pet: Pet;
}
interface IPet {
type: 'dog' | 'cat';
name: string;
}
class Person extends GraphNode<IPerson> {}
class Pet extends GraphNode<IPet> {}
Example usage:
const spot = new Pet()
.set('type', 'dog')
.set('name', 'Spot');
const jo = new Person()
.set('name', 'Jo')
.set('age', 41)
.set('pet', spot) // ⚠️ Should not accept GraphNode argument.
.setRef('pet', spot); // ✅ Correct.
const sam = new Person()
.set('name', 'Sam')
.set('age', 45)
.set('friend', jo) // ⚠️ Should not accept GraphNode argument.
.setRef('friend', jo); // ✅ Correct.
My problem is this — how can I define the GraphNode base class, when its generic types depend recursively on the GraphNode class? Here's my attempt:
Utility Types:
type Literal = null | boolean | number | string;
type LiteralAttributes<Base> = {
[Key in keyof Base]: Base[Key] extends Literal ? Base[Key] : never;
};
// ⚠️ ERROR — Generic type 'GraphNode<Attributes>' requires 1 type argument(s).
type RefAttributes<Base> = {
[Key in keyof Base]: Base[Key] extends infer Child
? Child extends GraphNode | null
? Child | null
: never
: never;
};
Abstract Graph Definition:
type GraphAttributes = {[key: string]: Literal | GraphNode | null};
abstract class GraphNode<Attributes extends GraphAttributes> {
public attributes = {} as LiteralAttributes<Attributes> | RefAttributes<Attributes>;
public get<K extends keyof LiteralAttributes<Attributes>>(key: K): Attributes[K] {
return this.attributes[key]; // ⚠️ Missing type information.
}
public set<K extends keyof LiteralAttributes<Attributes>>(key: K, value: Attributes[K]): this {
this.attributes[key] = value;
return this;
}
public getRef<K extends keyof RefAttributes<Attributes>>(key: K): Attributes[K] | null {
return this.attributes[key]; // ⚠️ Missing type information.
}
public setRef<K extends keyof RefAttributes<Attributes>>(key: K, value: Attributes[K] | null): this {
this.attributes[key] = value; // ⚠️ Missing type information.
return this;
}
}
That doesn't compile, and I've flagged the TS errors in comments above. I think that comes down to a basic problem – I want to extend the generic class later, with type arguments that refer to the generic class, and I'm not sure how to do that. I could live with not having strict type checking within the GraphNode class itself, but I really do want its subclasses to have strict type checking.
I've also created an example TypeScript playground reproducing this code with the compiler errors.