Rodrigo Leite
Rodrigo Leite

Reputation: 596

Property that indexes other property in Typescript

I have the following types:

interface CellsReducer {
    source: number;
    destination: number;
    plan: string;
    duration: number;
    test: []
}

interface BarReducer {
    baz: string;
}

interface AppState {
    cells: CellsReducer;
    bar: BarReducer;
}

I want to write an interface with the following objects:

interface Props {
    store: keyof AppState;
    field: // AppState[store]
    data: // AppState[store][field]
}

Using Generics didn't get me anywhere. fields ends up with type never in the following example:

type Stores<T> = keyof T;
type Fields<T> = keyof T[Stores<T>];
type Props<TState> = {
    state: Stores<TState>;
    field: Fields<TState>
}

Is there a way of doing this?

Upvotes: 2

Views: 51

Answers (2)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249606

You need distinct type parameters for each property in the path. This allows the compiler to reason about the specific fields you specify:

type Props<TState, KStore extends keyof TState, KField extends keyof TState[KStore]> = {
    state: KStore;
    field: KField
    data: TState[KStore][KField]
}

let p: Props<AppState, "cells", "duration"> = {
  state: "cells",
  field: "duration",
  data: 1
}

The reason you get never is because when the compiler tries to expand AppState[keyof AppState] it will get a union CellsReducer | BarReducer. Since only common members of union are accesible keyof (CellsReducer | BarReducer) is never (no keys are accessible).

The extra parameters capture the actual field, so if KStore is the string literal type "cells" keyof AppState["cells"] will be the keys of that particular field in app state. KField works similarly allowing us to correctly type data.

To avoid specifying the state and field values twice you can write a helper function:

function propertyFactory<TState>() {
  return function <KStore extends keyof TState, KField extends keyof TState[KStore]>(o: Props<TState, KStore, KField>) {
    return o;
  }
}
let p = propertyFactory<AppState>()({
  state: "cells",
  field: "duration",
  data: 1
})

Upvotes: 1

Karol Majewski
Karol Majewski

Reputation: 25790

Do you mean:

interface Props<T, K extends keyof T, V extends keyof T[K]> {
    state: keyof T;
    field: T[K];
    data: T[K][V]
}

Usage:

const props: Props<AppState, 'cells', 'plan'> = { /* ... */ } ;
const props: Props<AppState, 'bar', 'baz'> = { /* ... */ } ;

Upvotes: 1

Related Questions