Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: type that is union of object keys can't be use as a key for this object

Tags:

typescript

I have a function that is called with one parameter. This parameter is object. This function is returning another object, which one property is value of passed to function object. I have problem when I'm trying to retrieve value of object, using obj[key] notation.

I do understand, that key has to be proper type to be used like this. I can't use simple key as string, if in interface I don't have [key: string]: any. But that's not the issue. My key is union of strings that were keys in passed object: 'user' | 'name' etc.

interface Twitter<T> {
  name: T;
  user: T;
  sex: T extends string ? boolean : string;
}
interface Facebook<T> {
  appName: T;
  whatever: T;
  imjustanexapmle: T;
}

type TwFbObjKeys = keyof Twitter<string> | keyof Facebook<string>

My function looks like this:

public static getArrayOfObjects(
        queryParameters: Twitter<string> | Facebook<string>,
    ) {
        const keys = Object.keys(queryParameters) as TwFbObjKeys[];
        return keys.map((paramId) => ({
            paramId,
            value: queryParameters[paramId],
        }));
    }

I would expect, that using paramId which is type: 'name' | 'sex' | 'appName' | ... as a key for object, that has this keys, wouldn't throw an error. But unfortunately I have error:

TS7053: Element implicitly has an 'any' type because expression of type 'name' | 'sex' | 'appName' | ... can't be used to index type Twitter | Facebook. Property 'name' does not exists on type Twitter | Facebook

I'm fighting with it for few hours now. Any idea how I can solve it?

like image 703
Zenek Wiaderko Avatar asked Nov 07 '19 07:11

Zenek Wiaderko


1 Answers

Better define the function parameter queryParameters as generic type T with constraint <T extends Twitter<string> | Facebook<string>> instead of an union type Twitter<string> | Facebook<string>:

function getArrayOfObjects<T extends Twitter<string> | Facebook<string>>(
    queryParameters: T
) {
    const keys = Object.keys(queryParameters) as (keyof T)[];
    return keys.map((paramId) => ({
        paramId,
        value: queryParameters[paramId],
    }));
}

Playground

Explanations

By using generics you keep the type of the passed in argument for queryParameters and just ensure that it will be a sub type of Twitter<string> | Facebook<string>. paramId with type keyof T now can access queryParameters properties.

With a union type Twitter<string> | Facebook<string>, queryParameters would remain undetermined and could be still Twitter<string> or Facebook<string> in the function body. That causes problems with keyof operator and property access in your case, as you can only access common property keys of all constituents given the union type. And there are no common properties for Twitter<string> and Facebook<string>.

To illustrate the issue a bit more, you probably meant to define TwFbObjKeys to be

// "name" | "user" | "sex" | "appName" | "whatever" | "imjustanexapmle"
type TwFbObjKeys = keyof Twitter<string> | keyof Facebook<string>

// not this: keyof only applies to Twitter<string> here: "name" | "user" | "sex" | Facebook<string>
type TwFbObjKeys_not = keyof Twitter<string> | Facebook<string>

But that would not solve the issue alone. Example:

declare const queryParameters: Twitter<string> | Facebook<string>

// type keysOfQueryParameters = never
type keysOfQueryParameters = keyof typeof queryParameters

// ok, let's try to define all union keys manually
type TwFbObjKeys = "name" | "user" | "sex" | "appName" | "whatever" | "imjustanexapmle"
declare const unionKeys: TwFbObjKeys

// error: doesn't work either
queryParameters[unionKeys]

// this would work, if both Twitter and Facebook have "common" prop
queryParameters["common"]

Playground

With help of a generic type, we can use keyof T to safely access the passed in queryParameters keys.

like image 127
ford04 Avatar answered Sep 17 '22 22:09

ford04