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

Can't use indexed/mapped type signature in interfaces? #13573

Closed
sccolbert opened this issue Jan 19, 2017 · 19 comments
Closed

Can't use indexed/mapped type signature in interfaces? #13573

sccolbert opened this issue Jan 19, 2017 · 19 comments
Labels
Duplicate An existing issue was already created

Comments

@sccolbert
Copy link

TypeScript Version: 2.1.5

What I'd like to do is effectively extend from a record type and then add a few more custom properties. Since type aliases cannot by extended, I tried solving the problem with an indexed type signature in the interface. Unfortunately, this doesn't compile. I also can't add extra properties to a mapped type, so it seems I may be stuck. Is there a way around this problem?

Code

type Names = 'foo' | 'bar' | 'baz';

interface Thing1 {
  readonly [T in Names]?: string;
}

interface Thing2 {
  readonly [T in Names]?: string;
  extra?: number;
}

type Thing3 = Readonly<Partial<Record<Names, string>>>;

type Thing4 = {
  readonly [T in Names]?: string;
};

type Thing5 = {
  readonly [T in Names]?: string;
  extra?: number;
};

Expected behavior:
I would expect all of these to compile.

Actual behavior:
Thing1 and Thing2 fail to compile due to the indexed type signature. Thing5 fails to compile due to the extra property.

@sccolbert
Copy link
Author

It looks like this is a solution:

type Thing6 = Readonly<Partial<Record<Names, string>>> & { extra?: number };

However, I'm still curious if there is a better way.

@fcamblor
Copy link

fcamblor commented Feb 17, 2017

Personally, I would not like allowing to add extra properties on {type aliases|interfaces} having indexed definition, and not following this indexed definition constraint (in your case, your extra property is of type number, not following the string value indexed type constraint) as I would consider it as an important backward incompatible change in meaning for such indexed definitions.

I often use such type definition (with :, not the new in operator -but I think we could keep the same idea between indexed properties, no matter if we use : or in-) to say to TS compiler "hey, I'm gonna work with a map having every of its values of a certain type (...and optionally, I can give you a list of those properties to help intellisense)"

For instance :

type LogCallback = (message: string, context?: LogContext) => void;
interface LogLevelCallbacks {
    // Here, defining that "every properties of my map will be of type LogCallback"
    [key: string]: LogCallback;

    // Here, specifying the exhaustive list of my properties.. particularly useful to have completion in intellisense
    info: LogCallback;
    warn: LogCallback;
    error: LogCallback;
    
    blah: string; // Error, won't compile as it won't follow the guideline stated above : "every properties should be of type LogCallback"
}

let callbacks: LogLevelCallbacks = {
    info: console.info,
    warn: console.warn,
    error: console.error,

    // This seems allowed, however intellisense won't list foo after callbacks variable completion
    foo: console.info
};

let level = "info";
callbacks[level]("hello world"); // It does compile (and I have intellisense completion as well) thanks to indexed type declaration.

HOWEVER, if I disagree on your post statement to be able to "append" extra properties on indexed interfaces, I totally agree about the idea that it seems cumbersome to me to have the possibility to use in operator in an alias type and not in an interface (that's what your issue title denotes :-))

I typically don't know why I'm not able to simplify my interface above like this :

type LogLevel = "info"|"warn"|"error";
interface LogLevelCallbacks {
    // Can't do this
    [T in LogLevel]: LogCallback;
}

// However, this works and can be considered as a workaround even if less natural
type LogLevelCallbacksType = [T in LogLevel]: LogCallback;

I would still love to be able to declare Mapped Types in interfaces as it would open the doors to more complex constraints, particularly allowing me to specify common generics at the indexed definition level, and more precise type at the field level.

For instance, let's consider my LogCallback type may use Generics for their context closure parameter.
I may have this :

type LogCallback<T extends LogContext> = (message: string, context?: T) => void;
type LogLevel = "info"|"warn"|"error";
interface LogLevelCallbacks {
    [T in LogLevel]: LogCallback<LogContext>;

    info: LogCallback<InfoLogContext>;
    warn: LogCallback<WarnLogContext>;
    error: LogCallback<ErrorLogContext>;
}

Today, if I use : operator, I can declare such interface. However, I won't be able to tell my indexed key to be one of the LogLevel properties.
And on the other hand, I doubt we could declare such equivalent declaration with the type alias workaround form.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label May 24, 2017
@zheeeng
Copy link

zheeeng commented Oct 19, 2017

+1 Hope we can use 'in' operator in index signature while declaring interfaces not only types. We really need a mapped index sometimes. The inconsistency between interface and type looks confusing.
At present, I have to change an interface declaring to type which is surrounded by vast interfaces declaring.

@aaroncraig10e
Copy link

Is there any movement on this issue? It seems to be something that is extremely useful, and quite a common use case.

If there is no plan to support something like:

enum Types { Foo, Bar }
interface Things {
  [k in Types]: boolean;
  quu: string;
}

then is there a suggested alternative pattern that achieves the same goal -- namely being able to define an object shape where the key may be any of a list of acceptable values?

@ackvf
Copy link

ackvf commented May 25, 2018

@aaroncraig10e I was also looking for something like that. For now this intersection workaround does the job:

interface TT { 
  unit: string, 
  decimals: number, 
  prefix: string, 
  postfix: string,   
}

type MySymbols = 'BTC' | 'CZK' | 'ETH' | 'EUR' | 'LTC'

type S1 = {
  [P in MySymbols]: TT
}

interface S2 {
  format: (value: number, currency: MySymbols) => string
}

type S = S1 & S2 // intersection here

const SYMBOLS: S = {
  BTC: { unit: 'BTC', decimals: 5, prefix: '',  postfix: ''   },
  CZK: { unit: 'CZK', decimals: 2, prefix: '',  postfix: ',-' },
  ETH: { unit: 'ETH', decimals: 4, prefix: 'Ξ', postfix: ''   },
  EUR: { unit: 'EUR', decimals: 2, prefix: '€', postfix: ''   },
  LTC: { unit: 'LTC', decimals: 3, prefix: '',  postfix: ''   },
  format(value, currency) {
    const c = this[currency] // this gets inferred as any and not of type TT
    return c.prefix + value.toFixed(c.decimals) + c.postfix
  },
}

@mhegazy
Copy link
Contributor

mhegazy commented May 25, 2018

@ackvf use --noImplicitThis.

@ackvf
Copy link

ackvf commented May 25, 2018

@mhegazy It did help, but why? I disabled it previously as I had some issues with it, but I can't remember what.

/edit
I remember now. #10835

@mhegazy
Copy link
Contributor

mhegazy commented May 25, 2018

this in a function by default has type any. if you use --noImplicitThis, the compiler raises errors for untyped this, and tries to infer the type in some contexts, e.g. object literals.

but why?

cause it would have been a breaking change otherwise. this was always any before --noImplcitThis.

@mpawelski
Copy link

This proposal fells like natural extension of mapped types functionality. I guess many people try this exact proposed syntax to then see that it doesn't work.

In my case it would be very useful for situation when we want to extend 3rd party interface with properties that can be "computed" by mapped type. In theory we can achieve the result by some workarounds like writing interface that extends mapped type alias:
image

but this would be much nicer if I could write [P in Names] : string; inside interface.

I guess this issue is not a big priority for TS team because from what I can see there is nothing fundamentally new that you can't do today. For instance the examples from OP could be written as:

type Names = 'foo' | 'bar' | 'baz';

type Temp = { readonly [T in Names]?: string };

interface Thing1 extends Temp {}

interface Thing2 extends Temp {
  extra?: number;
}

type Thing3 = Readonly<Partial<Record<Names, string>>>;

type Thing4 = { readonly [T in Names]?: string };

type Thing5 = {
  extra?: number;
} & Temp;

There is just this confusion for new users about where you can use mapped type syntax and where not and IMO this confusion itself is worth to consider fixing it.

@m3hm3tk
Copy link

m3hm3tk commented Oct 14, 2018

I was also surprised when I discovered I cannot create mapped interfaces directly. The TS handbook says:

A second more important difference is that type aliases cannot be extended or implemented from (nor can they extend/implement other types). Because an ideal property of software is being open to extension, you should always use an interface over a type alias if possible.

I am happy this no longer seems to be the case

@bluelovers
Copy link
Contributor

hope can use in and keyof in interface

when type is string symbol number

@jameswilddev
Copy link

@mpawelski If you don't mind me asking, how did you do that? As far as I know, interfaces cannot extend from type declarations in TypeScript (TS2312).

@mpawelski
Copy link

@jameswilddev It was introduced in typescript 2.2.0. You will in fact get error TS2312: An interface may only extend a class or another interface in previous versions if you try to compile my example from previous post.

works fine:
npx "[email protected]" -skipLibCheck .\exampleFromMyPreviousPost.ts
gives TS2312 error:
npx "[email protected]" -skipLibCheck .\exampleFromMyPreviousPost.ts

You can read more details in Allow deriving from object and intersection types PR

@jameswilddev
Copy link

jameswilddev commented Nov 25, 2018

@mpawelski Aha, think it's just that I'm overbending the type system a bit. On 3.1.6, something closer to your example works:

type SingleKeyValueOf<T> = {
  [key in "A" | "B"]: string
}


interface X<T> extends SingleKeyValueOf<T> { }

However, this doesn't:

type SingleKeyValueOf<T> = {
  [key in keyof T]: string
}


interface X<T> extends SingleKeyValueOf<T> { }

Thanks.

@JasCodes
Copy link

any updates on this?

@dragomirtitian
Copy link
Contributor

dragomirtitian commented Mar 19, 2019

@Jas99 Not sure this will solve uses that rely on generic parameters, but I think the original code will work when #26797 gets merged.

@RyanCavanaugh
Copy link
Member

Tracking at #26797

@RyanCavanaugh RyanCavanaugh added Duplicate An existing issue was already created and removed Needs Investigation This issue needs a team member to investigate its status. labels Aug 22, 2019
@jcalz
Copy link
Contributor

jcalz commented Oct 19, 2021

Note that this isn't completely a duplicate of #26797, since that one would not have really supported mapped types (index signatures don't give you a handle on the elements in the set of keys). This issue seems to be the canonical one for "why can't we add extra properties next to mapped types" which isn't quite the same as "why can't we have arbitrary index signatures".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

15 participants