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?
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With