Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: keyof typeof union between object and primitive is always never

First, a bit of context to my question: I have a project in which I receive an object via Socket.IO, thus I have no type information about it. Additionally, it is a rather complex type, so there is a lot of checking going on to make sure the received data is good.

The problem is that I need to access properties of a local object specified by strings in the received object. This works OK for the first dimension as I can cast the property specifier of type any to keyof typeof whatever it is I want to access (e.g. this.property[<keyof typeof this.property> data.property]).

The type of the resulting variable is obviously a rather lengthy union type (uniting all the types of all the properties this.property has). As soon as one of those properties is of a non primitive type keyof typeof subproperty is inferred to be never.

Through the checking done before, I can guarantee that the property exists and I am 99% sure that the code would run once compiled. It is just the compiler that is complaining.

Below is some very simple code that reproduces this behaviour along with observed and expected types.

const str = 'hi';
const obj = {};
const complexObj = {
    name: 'complexObject',
    innerObj: {
        name: 'InnerObject',
    },
};

let strUnion: typeof str | string;              // type: string
let objUnion: typeof obj | string;              // type: string | {}
let complexUnion: typeof complexObj | string;   // type: string | { ... as expected ... }

let strTyped: keyof typeof str;                 // type: number | "toString" | "charAt" | ...
let objTyped: keyof typeof obj;                 // type: never (which makes sense as there are no keys)
let complexObjTyped: keyof typeof complexObj;   // type: "name" | "innerObject"

let strUnionTyped: keyof typeof strUnion;       // type: number | "toString" | ...
let objUnionTyped: keyof typeof objUnion;       // type: never (expected: number | "toString" | ... (same as string))
let complexUnionTyped: keyof typeof complexUnion;   // type: never (expected: "name" | "innerObject" | number | "toString" | ... and all the rest of the string properties ...)
let manuallyComplexUnionTyped: keyof string | { name: string, innerObj: { name: string }};  // type: number | "toString" | ... (works as expected)

Is this a known limitation of TypeScript (version 3) or am I missing something here?

like image 454
Jan Hettenkofer Avatar asked Mar 06 '23 11:03

Jan Hettenkofer


1 Answers

If you have a union, only common properties are accessible. keyof will give you the publicly accessible keys of a type.

For strUnionTyped which a union between string and the string literal type 'hi' the resulting type will have the same properties as string as the two types in the union have the same keys as string.

For objUnionTyped and complexUnionTyped the union has no common keys, so the result will be never

For manuallyComplexUnionTyped you get the keys of string, because what you wrote is actually (keyof string) | { name: string, innerObj: { name: string }} not keyof (string | { name: string, innerObj: { name: string }}) so you get the keys of string in a union with the object type you specified.

To get the keys of all members of a union you can use a conditional type:

type AllUnionMemberKeys<T> = T extends any ? keyof T : never;
let objUnionTyped: AllUnionMemberKeys<typeof objUnion>;
let complexUnionTyped: AllUnionMemberKeys<typeof complexUnion>;

Edit

The reason the conditional types help getting keys of all union members is because conditional types distribute over naked type parameters. So in our case

AllUnionMemberKeys<typeof objUnion> = (keyof typeof obj) | (keyof string)
like image 101
Titian Cernicova-Dragomir Avatar answered Apr 07 '23 16:04

Titian Cernicova-Dragomir