Brian
Brian

Reputation: 615

How to Infer Generic Property Types?

I can't figure out how to infer the type of a generic property based on the generic type of the object it's on. In the following example, how can I say that Something.aProp needs to match the type of Something's U.obj.prop?

interface Prop {
  a: number;
}
interface FancyProp extends Prop {
  b: number;
}

interface Obj<T extends Prop> {
  prop: T;
}

interface FancyObj extends Obj<FancyProp> {}

interface Parent<T extends Obj<any>> { // <-- the <any> here seems wrong too
  obj: T;
}

interface FancyParent extends Parent<FancyObj> {
  fancy: number;
}

class Something<U extends Parent<any>> {
  aProp: typeof U.obj.prop;
}

I.e. Something<Parent>.aProp should be of type Prop, and Something<FancyParent>.aProp is of type FancyProp?

Upvotes: 1

Views: 2124

Answers (1)

jcalz
jcalz

Reputation: 327624

For your main question, the way to look up the type of a property value given an object type T and a key type K is to use lookup types, a.k.a., indexed access types, via the bracket syntax T[K]. So if you want to look up the type of the "prop"-keyed property of the "obj"-keyed property of an object of type U, you would write that type as U["obj"]["prop"].

Note that dot syntax doesn't work for types, even if the key types are string literals. It would be nice if U.obj.prop were a synonym for U["obj"]["prop"] in the type system, but unfortunately that syntax would collide with namespaces, since there could be a namespace named U, with a subnamespace named obj, with an exported type named prop, and then U.obj.prop would refer to that type.


For your comments about any, it's not really wrong to use X extends Y<any> when Y<T>'s type parameter T has a generic constraint, but it might be a bit less type safe than you can get. If the type Y<T> is related to T in a covariant way, then you can use the generic constraint instead of any.

That would mean, for example, Parent<T extends Obj<any>> could be replaced with Parent<T extends Obj<Prop>>, and U extends Parent<any> could be replaced with U extends Parent<Obj<Prop>>.


Those changes give you code like this:

interface Parent<T extends Obj<Prop>> {
    obj: T;
}

class Something<U extends Parent<Obj<Prop>>> {
    aProp: U['obj']['prop'];
    constructor(u: U) {
        this.aProp = u.obj.prop;
    }
}

I also added a constructor to Something because class properties should be initialized and I wanted to show that aProp could be assigned with a value from u.obj.pop when u is a U.

And this should work as you expect:

interface PlainObj extends Obj<Prop> { }
interface PlainParent extends Parent<PlainObj> { }
new Something<PlainParent>({ obj: { prop: { a: 1 } } }).aProp.a; // number

interface FancyObj extends Obj<FancyProp> { }
interface FancyParent extends Parent<FancyObj> {
    fancy: number;
}
new Something<FancyParent>({ obj: { prop: { a: 1, b: 2 } }, fancy: 3 }).aProp.b; // number

Okay, hope that helps; good luck!

Playground link to code

Upvotes: 1

Related Questions