1

I have the following type definitions.

type Key<Row> = {
  [P in keyof Row]: Row[P] extends string ? P : never
}[keyof Row];

type ID<Row> = Row[Key<Row>];

type Selected<Row> = {
  selected: boolean;
} & Row;

Key<Row> returns all the property keys of Row whose values are strings. See this answer for how it is constructed.

ID<Row> is a union of all the string values of Row. It is always a string but narrows nicely if the string value is a string constant, e.g. in

interface X {
  prop: "abc";
}

ID<X> is always "abc".

Selected<Row> is an intersection type of whatever Row is plus the property selected: boolean.

With all the above defined, I don't understand why the below function definition throws an error:

const getId = <Row>(row: Selected<Row>, key: Key<Row>): ID<Selected<Row>> =>
  row[key];

The type error TS gives me is pasted below, but I can't work out why TS doesn't like my code.

From what I can see row[key] should always work because even if row is Selected<Row>, Key<Row> only contains a narrower set of keys, so what's the problem with keying into an object that has more properties than the key we're actually using?

Here is a link to the TS playground with this code showing the error


Type 'Selected<Row>[{ [P in keyof Row]: Row[P] extends string ? P : never; }[keyof Row]]' is not assignable to type '{ selected: boolean; }[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)] & Row[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<...>["selected"] extends string ? "selected" : never)]'.
  Type 'Selected<Row>[Row[keyof Row] extends string ? keyof Row : never]' is not assignable to type '{ selected: boolean; }[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)] & Row[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<...>["selected"] extends string ? "selected" : never)]'.
    Type 'Selected<Row>[keyof Row]' is not assignable to type '{ selected: boolean; }[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)] & Row[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<...>["selected"] extends string ? "selected" : never)]'.
      Type 'Row[string] | Row[number] | Row[symbol]' is not assignable to type '{ selected: boolean; }[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)] & Row[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<...>["selected"] extends string ? "selected" : never)]'.
        Type 'Row[string]' is not assignable to type '{ selected: boolean; }[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)] & Row[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<...>["selected"] extends string ? "selected" : never)]'.
          Type 'Row[string]' is not assignable to type '{ selected: boolean; }[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)]'.
            Type 'Row' is not assignable to type '{ selected: boolean; }'.
              Type 'Row[string]' is not assignable to type 'boolean'.
                Type 'Selected<Row>[keyof Row]' is not assignable to type '{ selected: boolean; }[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)]'.
                  Type 'keyof Row' is not assignable to type '(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)'.
                    Type 'keyof Row' is not assignable to type 'Selected<Row>[keyof Row] extends string ? keyof Row : never'.
                      Type 'Selected<Row>[keyof Row]' is not assignable to type 'boolean'.
                        Type 'Selected<Row>[Row[keyof Row] extends string ? keyof Row : never]' is not assignable to type '{ selected: boolean; }[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)]'.
                          Type 'Row[keyof Row] extends string ? keyof Row : never' is not assignable to type '(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)'.
                            Type 'Row[keyof Row] extends string ? keyof Row : never' is not assignable to type 'Selected<Row>["selected"] extends string ? "selected" : never'.
                              Type 'Selected<Row>[Row[keyof Row] extends string ? keyof Row : never]' is not assignable to type 'boolean'.
                                Type 'Selected<Row>[{ [P in keyof Row]: Row[P] extends string ? P : never; }[keyof Row]]' is not assignable to type '{ selected: boolean; }[(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)]'.
                                  Type 'Row[keyof Row] extends string ? keyof Row : never' is not assignable to type '(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)'.
                                    Type 'keyof Row' is not assignable to type '(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)'.
                                      Type 'string | number | symbol' is not assignable to type '(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)'.
                                        Type 'string' is not assignable to type '(Selected<Row>[keyof Row] extends string ? keyof Row : never) & (Selected<Row>["selected"] extends string ? "selected" : never)'.
                                          Type 'string' is not assignable to type 'Selected<Row>[keyof Row] extends string ? keyof Row : never'.
                                            Type 'keyof Row' is not assignable to type 'Selected<Row>[keyof Row] extends string ? keyof Row : never'.
                                              Type 'string | number | symbol' is not assignable to type 'Selected<Row>[keyof Row] extends string ? keyof Row : never'.
                                                Type 'string' is not assignable to type 'Selected<Row>[keyof Row] extends string ? keyof Row : never'.
                                                  Type 'Row[keyof Row] extends string ? keyof Row : never' is not assignable to type 'Selected<Row>["selected"] extends string ? "selected" : never'.
                                                    Type 'Selected<Row>[{ [P in keyof Row]: Row[P] extends string ? P : never; }[keyof Row]]' is not assignable to type 'boolean'.
                                                      Type 'Selected<Row>[Row[keyof Row] extends string ? keyof Row : never]' is not assignable to type 'boolean'.
                                                        Type 'Selected<Row>[keyof Row]' is not assignable to type 'boolean'.
                                                          Type 'Row[string] | Row[number] | Row[symbol]' is not assignable to type 'boolean'.
                                                            Type 'Row[string]' is not assignable to type 'boolean'.
Aron
  • 8,696
  • 6
  • 33
  • 59

1 Answers1

2

The compiler isn't as clever as you are, especially when it comes to reasoning about conditional types that depend on unresolved generic parameters (like NonSelected<Row>[P] extends string ? P : never). If you've gone over the logic and are sure that what you're doing is safe, a judicious type assertion is warranted:

const getId = <Row>(row: Selected<Row>, key: Key<Row>) =>
  row[key] as unknown as ID<Selected<Row>>;

Or, you can give the compiler something it can reason about, such as that an object of type O indexed by a key of type K will produce a value of type O[K]:

const getId = <Row>(row: Selected<Row>, key: Key<Selected<Row>>): ID<Selected<Row>> =>
  row[key];

Either of those should appease the compiler and your examples continue to behave as expected.

Hope that helps. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Ok makes sense. Thank you. Btw you also answered my initial question about how to construct the `Key` type in the first place :) – Aron Jun 18 '19 at 08:44