Reputation: 2329
My question is a bit more complex so here are possibly all the required parts:
// Common interface with an id and a string lieral type
interface IHandler {
id: Id<IHandler>;
type: string;
}
// Base class for all the handler, with generic argument providing string literal type and access to inheriting type
class Base<Type extends string, T extends Base<Type, T>> implements IHandler {
id: Id<T> = Guid.raw() as any
// In consturctor string literal type is instantiated
protected constructor(public type: Type) {
this.type = type;
}
// Static function that retrieves handler by it's key from a store
static GetHandler: <T extends IHandler>(get: Id<T>) => T;
// Getter that accepts key of an id attribute and returns instance from store
get = <Handler extends Base<Handler["type"], Handler>>(key: HandlerIds<T, Handler>) => Base.GetHandler<Handler>((this as any)[key]);
}
// Keys of attributes that are Id's
type HandlerIds<T extends Base<T["type"], T>, U extends Base<U["type"], U>> = keyof SubType<Model<T>, Id<U>>;
// Creates a subtype based on condition
type SubType<Base, Condition> = Pick<Base, { [Key in keyof Base]: Base[Key] extends Condition ? Key : never }[keyof Base]>;
// Excludes attributes of Base
type Model<T extends Base<T["type"], T>> = Partial<Pick<T, Exclude<keyof T, keyof Base<T["type"], T>>>>;
// Type that holds guid reference and also a type that guid is supposed to be pointing to
type Id<T extends IHandler> = string & { __type: T }
So what I have is an interface
, then a base
class that implements a getter
, and then derived classes that pass their type to the base class. Also there is an Id
type the holds unique identifier of handler as well as it's type. Each handler has it's own id in id
variable and then can save references to other handlers in attribute of type Id
with generic argument of that handler's type.
What I would like to make work is to properly type the get
function so it infers the type of handler it is getting by provided key
. Since each key is templated by type of handler it is pointing to, this type should be probably used to infer the return type, but with setup like this it does not work.
You can see an example of desired usage here:
class Foo extends Base<"Foo", Foo> {
a: Id<Goo>;
b: Id<Foo>;
constructor() {
super("Foo");
// Get a reference to instance of "a" of type Goo
var aHandler = this.get("a");
// Get a reference to instance of "b" of type Foo
var bHandler = this.get("b");
}
}
class Goo extends Base<"Goo", Goo> {
constructor() {
super("Goo");
}
}
What I would like to achieve here is for the attributes aHandler
and bHandler
to be automatically inferred as Foo
and Goo
.
Upvotes: 1
Views: 2038
Reputation: 249636
You can user conditional types to extract the hander type from Id
. I also changed a bit the way you were getting the keys, it did not work with strict null checks :
// Common interface with an id and a string lieral type
interface IHandler {
id: Id<IHandler>;
type: string;
}
// Base class for all the handler, with generic argument providing string literal type and access to inheriting type
class Base<Type extends string, T extends Base<Type, T>> implements IHandler {
id: Id<T> = Guid.raw() as any
// In consturctor string literal type is instantiated
protected constructor(public type: Type) {
this.type = type;
}
// Static function that retrieves handler by it's key from a store
static GetHandler: <T extends IHandler>(get: Id<T>) => T;
// Getter that accepts key of an id attribute and returns instance from store
get<K extends HandlerIds<T, Array<Id<any>>>>(key: K, index: Extract<keyof T[K], number>) : HandlerFromIdArray<T[K]>
get<K extends HandlerIds<T, Id<any>>>(key: K) : HandlerFromId<T[K]>
get<K extends HandlerIds<T, Id<any>>>(key: K, index?: number) : IHandler {
return Base.GetHandler<IHandler>((this as any)[key]) as any; // this assertion is needed
}
}
// Keys of attributes that are Id's
type HandlerIds<T extends Base<T["type"], T>, C> = Exclude<{ [P in keyof T]-?: T[P] extends C ? P : never}[keyof T], keyof Base<string, any>>;
// Type that holds guid reference and also a type that guid is supposed to be pointing to
type Id<T extends IHandler> = string & { __type: T }
type HandlerFromId<T> = T extends Id<infer U> ? U: never;
type HandlerFromIdArray<T> = T extends Array<Id<infer U>> ? U: never;
class Foo extends Base<"Foo", Foo> {
a!: Id<Goo>;
b!: Id<Foo>;
c!: Id<Foo>[];
constructor() {
super("Foo");
// Get a reference to instance of "a" of type Goo
var aHandler = this.get("a");
// Get a reference to instance of "b" of type Foo
var bHandler = this.get("b");
var cHandler = this.get("c", 1); // Foo
}
}
class Goo extends Base<"Goo", Goo> {
constructor() {
super("Goo");
}
}
Upvotes: 3