Typescript: Quick fix for 'unions can't be used in index signatures, use a mapped object type instead'

Created on 17 May 2018  ·  37Comments  ·  Source: microsoft/TypeScript

The following code:

type K = "foo" | "bar";

interface SomeType {
    [prop: K]: any;
}

Gives this error message:

An index signature parameter type cannot be a union type. Consider using a mapped object type instead.

Nobody knows what mapped object types are, so let's give them a quick fix that

  • Switches the index signature to a mapped type
  • Moves other members to a separate object type that gets combined with an intersection type
  • Changes the containing object type to a type alias if the containing object type is an interface
  • Intersects with all the extends clauses if the containing object type is an interface and has any extends clauses
Error Messages Quick Fixes Moderate Fixed Suggestion help wanted

Most helpful comment

You can do this:

type Foo = 'a' | 'b';
type Bar = {[key in Foo]: any};

Though Bar has no index signature (i.e., you can't then do (obj as Bar)[value as Foo]).

Edit: Though if you could make the caveat a non-issue, I'd be eternally grateful!

All 37 comments

Nobody knows what mapped object types are, so let's give them a quick fix that

+1, just came here because I was expecting 2.9 to support unions as index signatures per your example code. I think this has been a long desired feature: #5683, #16760, etc..

You can do this:

type Foo = 'a' | 'b';
type Bar = {[key in Foo]: any};

Though Bar has no index signature (i.e., you can't then do (obj as Bar)[value as Foo]).

Edit: Though if you could make the caveat a non-issue, I'd be eternally grateful!

i'd like to work on this :laughing:

Moves other members to a separate object type that gets combined with an intersection type

what should we do if containing object type is an class?
I can only imagine that it is an interface

so what should follow code do after quickfix?

type K = "1" | "2"

class SomeType {
    a = 1;
    [prop: K]: any;
}

so what should follow code do after quickfix?

I would say this should not be fixable..

@mhegazy I'm using 3.0.0-rc and still getting the same error as originally posted. Is this expected?

I'm using 3.0.0-rc and still getting the same error as originally posted. Is this expected?

yes. the error is correct. this issue was tracking adding a quick fix for it, that is the light pulp next to the error message.

no code actions available with 2.9.1 and vscode

@ThaJay We won't backport this feature, try setting up a newer version.

Obviously. I'm sorry for not checking the timeline first, just assumed it would be new enough. New to ts. Will check with version 3.

how to describe this:

function createRequestTypes(base){
  return ['REQUEST', 'SUCCESS', 'FAILURE'].reduce((acc, type) => {
    acc[type] = `${base}_${type}`
    return acc
  }, {})
}

const user = createRequestTypes('USER')
console.log(user.REQUEST) // error
// just string? like:
interface IRequestType: {[key: string]: string}

I tried below, all failed:

type requestStatus = 'REQUEST' | 'SUCCESS' | 'FAILURE'
type requestTypes = {
  [key in requestStatus]: string
}
// or
interface IRequestTypes {[key: keyType]: string}
// or even
type requestTypes = {
  FAILURE: string,
  SUCCESS: string,
  REQUEST: string
}

@maicWorkGithub here you go:

const user = createRequestTypes('USER')
console.log(user.REQUEST) 

function createRequestTypes(base:string):requestTypes {
  const result : requestTypes    = {}
  const arr    : requestStatus[] = ['REQUEST', 'SUCCESS', 'FAILURE']  

  return arr.reduce((acc, type) => {
    acc[type] = `${base}_${type}`
    return acc
  }, result)
}


type requestStatus = 'REQUEST' | 'SUCCESS' | 'FAILURE'
type requestTypes = { [key in requestStatus]?: string }

@ihorskyi Thanks!!

Just curious why type works, but interface doesn't. Can someone explain, please? What's the reason for such a limitation (or a feature?) of interface.

type Foo = 'a' | 'b';
type Bar = {[key in Foo]: any}; // ok
interface Baz {[key in Foo]: any} // =>

// A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1169)
// A computed property name must be of type 'string', 'number', 'symbol', or 'any'.ts(2464)
// 'Foo' only refers to a type, but is being used as a value here.ts(2693)

This was an amazing auto-fix to discover. Thank you for implementing it! :)

Same for classes.

You can do this:

type Foo = 'a' | 'b';
type Bar = {[key in Foo]: any};

Though Bar has no index signature (i.e., you can't then do (obj as Bar)[value as Foo]).

Edit: Though if you could make the caveat a non-issue, I'd be eternally grateful!

Use Record instead!

type Foo = 'a' | 'b'
type Bar = Record<Foo, any>

To add one more example of this using a class...

class Foo {
   a: string;
   b: string;
}

type Bar = {[key in keyof Foo]: any};

its even better when using Partial

type A = 'x' | 'y' | 'z';
type M = Partial<{
    [key in A]: boolean
}>;

Nobody knows what mapped object types are, so let's give them a quick fix that

@DanielRosenwasser
Why can't the error message suggest the answer e.g. show a quick example using mapped type - that would only be couple of lines of code which will be consistent with the average length of Typescript error messages :trollface:

does anyone know if is possible to say that the interface which uses the type or enum as key can accept only one property?

For example a signature like this:{ <field>: { <and|or|xor>: <int> } } took from mongo bitwise operator.

export enum BitwiseOperator {
    and = "and",
    or = "or",
    xor = "xor",
}

export type BitwiseCondition = {
    [key in BitwiseOperator]?: number;
}

An then when using it, I would like to validate that the variable which is defined by the interface, has only one property.

const query: BitwiseCondition = {
  and: 5,
  or: 6  // raise a ts error
};

@b4dnewz You can't do it in Typescript. Workaround: https://github.com/Microsoft/TypeScript/issues/10575

@b4dnewz, if you only want 1 property, why not do it like this?

export enum BitwiseOperator {
  and = "and",
  or = "or",
  xor = "xor",
}

export type BitwiseCondition = {
  operator: BitwiseOperator;
  value: number;
}

@benwinding unfortunately the returned shape is different from what mongodb expects

@apieceofbart thanks for the suggestion, I've looked into it, a bit redundant in terms of interfaces but can work, I'm not sure if I'll implement it now, since it's not a big deal if the final user tries a bitwise condition with two operators, mongo will throw an error anyway

I'm trying to keep the mongo-operators definitions as simple as possible to avoid me headaches 😁 maybe in future a proper support is added

@b4dnewz fair enough,

Perhaps a simpler option you might be able to use is:

export type BitwiseCondition =
  | { or: number }
  | { xor: number }
  | { and: number }

That's about the closest you'll get without too much duplication

@b4dnewz fair enough,

Perhaps a simpler option you might be able to use is:

export type BitwiseCondition =
  | { or: number }
  | { xor: number }
  | { and: number }

That's about the closest you'll get without too much duplication

This will not yield error in this example:

const query: BitwiseCondition = {
  and: 5,
  or: 6  // raise a ts error
};

I thought that's the whole point

@apieceofbart,

This will not yield error in this example:

export type BitwiseCondition =
  | { or: number }
  | { xor: number }
  | { and: number }

const query: BitwiseCondition = {
  and: 5,
  or: 6  // doesn't raise a ts error!
};

Woah! that's weird :open_mouth: I did not know that!

It's seems that Typescript doesn't support mutually exclusive types for objects. It's also was proposal for the language here: https://github.com/microsoft/TypeScript/issues/14094

It is still technically possible though...

From this stackoverflow answer this is possible to achieve this using conditional types (the hardest types), but it aint pretty....

/*
 XOR boiler plate
*/
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = T | U extends object
  ? (Without<T, U> & U) | (Without<U, T> & T)
  : T | U;
type XOR3<S, T, U> = XOR<S, XOR<T, U>>;

// Code start
export type BitwiseCondition = XOR3<
  { or: number },
  { xor: number },
  { and: number }
>;

const query1: BitwiseCondition = {
  and: 5
};

const query: BitwiseCondition = {
  and: 5,
  or: 6 // raise a ts error
};

If anyone could make this prettier or better, please do

@mvasin FWIW, this _appears_ to achieve the same result, but I agree entirely that it should be a feature of interfaces just as it is on types.

type Foo = 'a' | 'b';

type Bar = {
  [key in Foo]: any
}

interface A extends Bar { }

class Wol implements A{
  a: any;
  b: any;
}

For typescript 3.5, it seems like I have to do this:

export interface DataTableState {
  columnStats: {[key in keyof DataTable]?:{}}
}

Is this the best way to do this?

Why exactly can't an index signature use an enum type? The mapped type almost does what I want, but then TypeScript expects every string from the enum to exist as a defined key. I don't actually want to assert that every key exists, more that if any keys do exist, they must live in the enum.

For example for the type:

type MyType = {
  [Key: 'foo' | 'bar' | 'zip']: number;
};

This should satisfy:

const x: MyType = {
  foo: 1,
  zip: 2
};

While I could just set the other keys undefined for a mapped type, I prefer to make the keys optional, but if they're present, the value cannot be undefined. If I make the mapped type values optional the code works but the types are less strong.

its even better when using Partial

type A = 'x' | 'y' | 'z';
type M = Partial<{
    [key in A]: boolean
}>;

Thanks!
Useful when you need to define a type that partially matches a dictionary

"Partial" can be used on Records too:

type Foo = 'a' | 'b';
let foo1: Record<Foo, number> = { a: 1, b: 2 };
let foo2: Partial<Record<Foo, number>> = { a: 1 };

I find myself unwittingly visiting this GitHub page every month or so.

My latest one is a real simple one:

interface ObjectLiteral {
    [key: string | number]: any
}
export const mapToObjectLiteral = (map: Map<string|number, any>) =>
    Array.from(map).reduce((objLit, [key, value]) => {
        objLit[key] = value
        return objLit
    }, {} as ObjectLiteral)

image

I can scroll up and figure out a workaround, but just wanted to provide feedback that this issue happens frequently in day to day work in slightly different scenarios.

here is an example:

type MapKey = string | number;
type ObjectLiteral<T extends MapKey, V = any> = {
  [P in T extends number ? string : T]: V;
};

export const mapToObjectLiteral = <T extends MapKey, V>(map: Map<T, V>) =>
  Array.from(map).reduce((objLit, [key, value]) => {
    objLit[key as keyof ObjectLiteral<T>] = value;
    return objLit;
  }, {} as ObjectLiteral<T, V>);

// how to make a better type of map ?
const m = new Map<1 | "foo", "a" | "b">();
m.set(1, "a");
m.set("foo", "b");

const o = mapToObjectLiteral(new Map(m));

console.log(o[1], o.foo); // just got an union type of every member of 'o'

https://github.com/microsoft/TypeScript/issues/24220#issuecomment-504285702

To add one more example of this using a class...

class Foo {
   a: string;
   b: string;
}

type Bar = {[key in keyof Foo]: any};

Very useful. Thanks! 🚀

Was this page helpful?
0 / 5 - 0 ratings

Related issues

coreh picture coreh  ·  3Comments

dlaberge picture dlaberge  ·  3Comments

harukaeru picture harukaeru  ·  3Comments

pjmolina picture pjmolina  ·  3Comments

donaldpipowitch picture donaldpipowitch  ·  3Comments