Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
DanielRosenwasser opened this issue May 17, 2018 · 37 comments
Labels
Domain: Error Messages The issue relates to error messaging Domain: Quick Fixes Editor-provided fixes, often called code actions. Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Fixed A PR has been merged for this issue Help Wanted You can do this Suggestion An idea for TypeScript

Comments

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented May 17, 2018

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
@DanielRosenwasser DanielRosenwasser added the Domain: Quick Fixes Editor-provided fixes, often called code actions. label May 17, 2018
@DanielRosenwasser DanielRosenwasser added this to the TypeScript 3.0 milestone May 17, 2018
@DanielRosenwasser DanielRosenwasser added Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Help Wanted You can do this Suggestion An idea for TypeScript labels May 17, 2018
@dannycochran
Copy link

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..

@mattbasta
Copy link

mattbasta commented May 18, 2018

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!

@Kingwl
Copy link
Contributor

Kingwl commented May 20, 2018

i'd like to work on this 😆

@Kingwl
Copy link
Contributor

Kingwl commented May 21, 2018

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;
}

@mhegazy
Copy link
Contributor

mhegazy commented May 21, 2018

so what should follow code do after quickfix?

I would say this should not be fixable..

@ghost ghost closed this as completed in #24286 May 23, 2018
@mhegazy mhegazy added the Fixed A PR has been merged for this issue label May 23, 2018
@dannycochran
Copy link

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

@mhegazy
Copy link
Contributor

mhegazy commented Jul 18, 2018

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.

@ThaJay
Copy link

ThaJay commented Aug 15, 2018

no code actions available with 2.9.1 and vscode

@ghost
Copy link

ghost commented Aug 15, 2018

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

@ThaJay
Copy link

ThaJay commented Aug 15, 2018

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.

@maicWorkGithub
Copy link

maicWorkGithub commented Sep 20, 2018

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
}

@ihorskyi
Copy link

ihorskyi commented Sep 23, 2018

@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 }

@maicWorkGithub
Copy link

@ihorskyi Thanks!!

@mvasin
Copy link

mvasin commented Dec 21, 2018

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)

@dgreene1
Copy link

dgreene1 commented Jan 4, 2019

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

@jackkoppa jackkoppa unassigned ghost Apr 16, 2019
@DanielRosenwasser DanielRosenwasser added the Domain: Error Messages The issue relates to error messaging label Apr 16, 2019
@Bessonov
Copy link

Same for classes.

@maoxiaoke
Copy link

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>

@benwinding
Copy link

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

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

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

@Somebi
Copy link

Somebi commented Sep 18, 2019

No words, just meme:
https://media1.tenor.com/images/23d9d746fc87b3a93298af43dae21f6a/tenor.gif

:)

@therepo90
Copy link

its even better when using Partial

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

@apieceofbart
Copy link

apieceofbart commented Nov 21, 2019

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:

@b4dnewz
Copy link

b4dnewz commented Dec 24, 2019

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
};

@apieceofbart
Copy link

@b4dnewz You can't do it in Typescript. Workaround: #10575

@benwinding
Copy link

@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;
}

@b4dnewz
Copy link

b4dnewz commented Dec 25, 2019

@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

@benwinding
Copy link

@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

@apieceofbart
Copy link

@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

@benwinding
Copy link

@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 😮 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: #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

@daverickdunn
Copy link

@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;
}

@chriszrc
Copy link

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?

@benwiley4000
Copy link

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.

@defusioner
Copy link

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

@ruojianll
Copy link

@ZYinMD
Copy link

ZYinMD commented Aug 2, 2020

"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 };

@breck7
Copy link

breck7 commented Oct 16, 2020

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.

@iahu
Copy link

iahu commented Oct 22, 2020

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'

@nelson6e65
Copy link

nelson6e65 commented Nov 1, 2020

#24220 (comment)

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! 🚀

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: Error Messages The issue relates to error messaging Domain: Quick Fixes Editor-provided fixes, often called code actions. Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Fixed A PR has been merged for this issue Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests