Lewis
Lewis

Reputation: 77

Typescript - Determine type of child object from given key

I'm trying to get intellisense working with an interface with a generic type, I'd like intellisense to and typescript checking to work for the properties of the children but I can't figure out how to write my interface.

This is a very simplified example of what I'm trying to do

interface Foo1 {
    prop1: string;
    prop2: boolean;
}

interface Foo2 {
    prop3: string;
    prop4: boolean;
}

interface Bar1 {
    thing1: Foo1;
    thing2: Foo2;
}
interface Bar2 {
    thing3: Foo1;
    thing4: Foo2;
}

Given a structure like this (in my project there are alot more Foo's Bar's and they all have far more properties) I want to be able to use the chosen key to produce something like this

interface TestItem<T> {
    field: keyof T; //This will be a key of a Bar object
    select: (keyof T[keyof T])[]; //This is trying to state that elements in this array must be a property of Bar[field], e.g if field is "thing1" the only valid elements would be "prop1" or "prop2"
}

interface TestObject<T> {
    itemArray: TestItem<T>[];
}


let test: TestObject<Bar1> = {
            itemArray: [
                {
                    field: "thing1",
                    select: ["prop1"] //error: string is not assignable to type never
                }, {
                    field: "thing2",
                    select: ["prop3"] //error: string is not assignable to type never
                }
            ]
        }

I didn't expect this to work but I've tried so many things with no better results. I know I could just add more GenericType arguments and specify what is getting passed in to each TestItem but I'm hoping someone knows a way that specifying the field will be enough for Typescript to work out the valid values for select in TestItem.

Upvotes: 2

Views: 4807

Answers (2)

artem
artem

Reputation: 51609

It can't be done with a single-step object assignment - there is no way to restrict type of an object property based on values of other properties in object initialization.

But with slightly different syntax it's possible:

class  TestObject<T> {

  itemArray: { field: keyof T, select: string[] }[];
  // no constraint could be specified for select here
  // because there is not enough information about T here

  // but we can define a function that will apply constraint
  // when called from a subclass
  testItem<K extends keyof T>(item: { field: K, select: (keyof T[K])[] }) {
    return item
  }
}

class TestBar1 extends TestObject<Bar1> {
  itemArray = [
    this.testItem({ field: "thing1", select: ["prop1"] }), // ok
    this.testItem({ field: "thing2", select: ["prop3"] }), // ok

    this.testItem({ field: "thing1", select: ["prop3"] })  // error:
    // Argument of type '{ field: "thing1"; select: "prop3"[]; }'is not assignable 
    // to parameter of type '{ field: "thing1"; select: ("prop1" | "prop2")[]; }'.
    //  Types of property 'select' are incompatible.
    //    Type '"prop3"[]' is not assignable to type '("prop1" | "prop2")[]'.
    //      Type '"prop3"' is not assignable to type '"prop1" | "prop2"'.
  ]
}

let test = new TestBar1().itemArray;

when the erroneous item is removed, the type inferred for test is as expected:

 ({ field: "thing1"; select: ("prop1" | "prop2")[]; } 
| { field: "thing2"; select: ("prop3" | "prop4")[]; })[]

Upvotes: 2

Johannes Klau&#223;
Johannes Klau&#223;

Reputation: 11020

Not quite sure if I understand your question correctly, but this works:

interface Foo1 {
    prop1: string;
    prop2: boolean;
}

interface Foo2 {
    prop3: string;
    prop4: boolean;
}

interface Bar1 {
    thing1: Foo1;
    thing2: Foo2;
}
interface Bar2 {
    thing3: Foo1;
    thing4: Foo2;
}

interface TestItem<T, K> {
    field: keyof T;
    select: (keyof K)[]; //This keyof T being the same as field
}

interface TestObject<T, K> {
    itemArray: TestItem<T, K>[];
}

let test: TestObject<Bar1, Foo1> = {
    itemArray: [
        {
            field: "thing1",
            select: ["prop1"]
        }, {
            field: "thing2",
            select: ["prop2"]
        }
    ]
}

Your example couldn't work because neither prop1 nor prop2 are owned by Bar1, so you have to extend your generic by another one (using K here)

Upvotes: 0

Related Questions