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

typescript keyof
typescript advanced types
typescript record
typescript conditional types
typescript create type from object keys
typescript union types
typescript object.keys keyof
typescript object key type

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?

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.

Quick fix for 'unions can't be used in index signatures, use a mapped , Consider using a mapped object type instead. Intersection Type, properties not excluded from index signature #24300 The mapped type almost does what I want, but then TypeScript expects every string from the enum to� We can use the keyof operator to retrieve a union of string literal types that contains all property keys of this object type: type UserKeys = keyof User; // This is equivalent to: type UserKeys = "id" | "name" | "email"; Next, we need to be able to exclude a specific string literal type from a union of string literal types.

I think your problem is coming from that line

type TwFbObjKeys = keyof Twitter<string> | Facebook<string>
// is of type type TwFbObjKeys = "name" | "user" | "sex" | Facebook<string>

you probably want

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

Advanced Types � TypeScript, Optional parameters and properties; Type guards and type assertions. Type Aliases That means an object of this type will have all members of all three types. You will Instead of any , we can use a union type for the padding parameter: A union type describes a value that can be one of several types. We use the vertical bar (|) to separate each type, so number | string | boolean is the type of a value that can be a number, a string, or a boolean. If we have a value that has a union type, we can only access members that are common to all types in the union.

(Assuming you means type TwFbObjKeys = keyof Twitter<string> | keyof Facebook<string>)

Typescript raise an error because paramId has the type which contains all the keys of both interfaces:

'name' | 'user'| 'sex' | 'appName' | 'whatever' | 'imjustanexapmle'

but queryParameters can only be a type of either one interface:

'name' | 'user'| 'sex'

or

'appName' | 'whatever' | 'imjustanexapmle'

To solve the problem, you should separate both types:

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

function isTwitter<T>(val: Twitter<T> | Facebook<T>): val is Twitter<T> {
  return (
    'name' in (val as Twitter<T>) &&
    'user' in (val as Twitter<T>) &&
    'sex' in (val as Twitter<T>) 
  )
}

function isFacebook<T>(val: Twitter<T> | Facebook<T>): val is Facebook<T> {
  return (
    'appName' in (val as Twitter<T>) &&
    'whatever' in (val as Twitter<T>) &&
    'imjustanexapmle' in (val as Twitter<T>) 
  )
}

function getValue<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

function getArrayOfObjects(
    queryParameters: Twitter<string> | Facebook<string>,
) {
  if (isTwitter(queryParameters)) { // if queryParameters is of type Twitter<string>
    const keys = Object.keys(queryParameters);
    return keys.map((paramId) => ({
      paramId,
      value: getValue(queryParameters, paramId as keyof Twitter<string>)
    }));
  } else if (isFacebook(queryParameters)) { // if queryParameters is of type Facebook<string>
    const keys = Object.keys(queryParameters);
    return keys.map((paramId) => ({ 
      paramId,
      value: getValue(queryParameters, paramId as keyof Facebook<string>)
    }));
  } else {  // if queryParameters is invalid
    throw new Error("invalid queryParameters!")
  }
}

keyof and Lookup Types in TypeScript — Marius Schulz, TypeScript 2.1 introduced the keyof operator and lookup types, which help capture Different properties on an object can have totally different types, and we don't all its property keys, which is a union of string literal types: Different properties on an object can have totally different types, and we don't even know what obj looks like. So how could we type this function in TypeScript? Here's a first attempt: function prop(obj: {}, key: string) { return obj[key]; } With these two type annotations in place, obj must be an object and key must be a string. We've now

More advanced types with TypeScript generics, To do so, we also explore union types, the keyof keyword, and string literal types. When we return object[property], the TypeScript compiler performs a get: < PropertyType extends keyof ObjectType>(key: PropertyType)� (That string-key constraint is important because objects in TypeScript can currently only use strings or numbers as keys; whereas union types can be all sorts of things.) Apart from that constraint on the union, though, we can basically just substitute a generic type parameter U, for “union,” where we had StateUnion before.

TypeScript explained in JavaScript: keyof, Exploring TypeScript's keyof operator using JavaScript. This series will assume you have some familiarity with Javascript but only the basics of TypeScript. If you' ve gone This returns an array of the object's properties (or keys). In this This returns the union type of all the properties in the User interface. The key here is the type of this function: toBeDetermined is Animal. Typescript understands that if we return true , the argument is an Animal. We can now use this variable:

Flattening Typescript Union Types, In this post I describe how to flatten a union of two objects into one Union Types in Typescript are a powerful way to describe a value that In this case, only shared properties will be accessible on the union type, unless you� All of this works because TypeScript allows us to index any object as long as the index's type is a union of all the possible keys, so it knows that the key is valid. Maybe in the future, using key in obj will work on its own, but until then, the helper function works well enough.

Comments
  • How do you call the method? getArrayOfObjects('appName') or getArrayOfObjects({appName: 'asd', .... })?
  • It was getArrayOfObject({name: 'Jon', sex: 'male', ...}) @ford04 had the answer.
  • Thank you! Missing keyof was a typo on my part when 'copying' code here, but it didn't work nevertheless (as you explained why). When I put your improvements, the error is no longer here. Thanks for wholesome answer. Not only the solution, but really the answer. Not only I have code without errors, but also I learnt something new. I really appreciate it.
  • Thanks by catching this! In my code I had keyof Facebook<string>. The issue is somewhere else. I will update my question.