TypeScript: Index signature is missing in type (only on interfaces, not on type alias)
TypeScript Version: 2.2.2
Code
interface IndexType {
[key: string]: string;
}
interface doesNotWork {
hola: string;
}
type doWorks = { hola: string };
let y: IndexType;
const correctA = { hola: "hello" };
const correctB: doWorks = { hola: "hello" };
//error should be assignable to y
const error: doesNotWork = { hola: "hello " };
y = correctA;
y = correctB;
y = error; //Index signature is missing in type 'doesNotWork'
y = {... error}; //workaround but not equivalent since the instance is not the same
Expected behavior:
The code should not result on a compiler error since the interface doesNotWork is equivalent to the type { hola: string }
Actual behavior:
Variable error of type doesNotWork can’t be assigned to y
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 370
- Comments: 91 (14 by maintainers)
Links to this issue
Commits related to this issue
- Allow types rather than interfaces See: https://github.com/microsoft/TypeScript/issues/15300 — committed to statechannels/wallet-specs by andrewgordstewart 5 years ago
- Allow types rather than interfaces See: https://github.com/microsoft/TypeScript/issues/15300 — committed to statechannels/wallet-specs by andrewgordstewart 5 years ago
- Allow types rather than interfaces See: https://github.com/microsoft/TypeScript/issues/15300 — committed to statechannels/statechannels by andrewgordstewart 5 years ago
- fix: more specific interfaces and index signatures A more specific interface can't be assigned to a more generic one if the generic one has an index signature. Workaround used from https://github.com... — committed to eps1lon/material-ui by eps1lon 4 years ago
- fix: more specific interfaces and index signatures A more specific interface can't be assigned to a more generic one if the generic one has an index signature. Workaround used from https://github.com... — committed to eps1lon/material-ui by eps1lon 4 years ago
- fix: more specific interfaces and index signatures A more specific interface can't be assigned to a more generic one if the generic one has an index signature. Workaround used from https://github.com... — committed to eps1lon/material-ui by eps1lon 4 years ago
- fix: more specific interfaces and index signatures A more specific interface can't be assigned to a more generic one if the generic one has an index signature. Workaround used from https://github.com... — committed to eps1lon/material-ui by eps1lon 4 years ago
- fix: more specific interfaces and index signatures A more specific interface can't be assigned to a more generic one if the generic one has an index signature. Workaround used from https://github.com... — committed to eps1lon/material-ui by eps1lon 4 years ago
- fix: more specific interfaces and index signatures A more specific interface can't be assigned to a more generic one if the generic one has an index signature. Workaround used from https://github.com... — committed to eps1lon/material-ui by eps1lon 4 years ago
- fix: more specific interfaces and index signatures A more specific interface can't be assigned to a more generic one if the generic one has an index signature. Workaround used from https://github.com... — committed to eps1lon/material-ui by eps1lon 4 years ago
- fix: more specific interfaces and index signatures A more specific interface can't be assigned to a more generic one if the generic one has an index signature. Workaround used from https://github.com... — committed to eps1lon/material-ui by eps1lon 4 years ago
- fix: more specific interfaces and index signatures A more specific interface can't be assigned to a more generic one if the generic one has an index signature. Workaround used from https://github.com... — committed to eps1lon/material-ui by eps1lon 4 years ago
- fix: more specific interfaces and index signatures A more specific interface can't be assigned to a more generic one if the generic one has an index signature. Workaround used from https://github.com... — committed to eps1lon/material-ui by eps1lon 4 years ago
- fix: more specific interfaces and index signatures A more specific interface can't be assigned to a more generic one if the generic one has an index signature. Workaround used from https://github.com... — committed to eps1lon/material-ui by eps1lon 4 years ago
- minor(web_ui): update jsonToFormData type to be more strict (#345) Currently using `object` which results in `any` and is too lenient for what [`FormData`](https://developer.mozilla.org/en-US/docs/W... — committed to chdsbd/kodiak by sbdchd 4 years ago
- Make the path param interface alias type becuase it's a bit more flexible when used. https://github.com/microsoft/TypeScript/issues/15300 — committed to eddeee888/route-codegen by eddeee888 4 years ago
- Make path params interface -> type alias (#54) ## Templates - Make the path param interface alias type becuase it's a bit more flexible when used. https://github.com/microsoft/TypeScript/issues/15... — committed to eddeee888/route-codegen by eddeee888 4 years ago
- https://github.com/microsoft/TypeScript/issues/15300 — committed to shocknet/wallet by deleted user 4 years ago
- fix(util-dynamodb): accept interface as in input for marshall Refs: https://github.com/microsoft/TypeScript/issues/15300#issuecomment-530667695 — committed to trivikr/aws-sdk-js-v3 by trivikr 3 years ago
- fix(util-dynamodb): accept interface/Class Instance as in input for marshall Refs: https://github.com/microsoft/TypeScript/issues/15300#issuecomment-530667695 — committed to trivikr/aws-sdk-js-v3 by trivikr 3 years ago
I was working on a somewhat related example today and I was pretty confused by the results. I tried to create some related situations so I could understand the rules.
As I understand:
I tested this in typescript playground:
Just to fill people in, this behavior is currently by design. Because interfaces can be augmented by additional declarations but type aliases can’t, it’s “safer” (heavy quotes on that one) to infer an implicit index signature for type aliases than for interfaces. But we’ll consider doing it for interfaces as well if that seems to make sense
This limitation also makes it practically infeasible to use a custom JSON type as a generic constraint for HTTP request libraries. Context: https://github.com/sindresorhus/ky/pull/80#issuecomment-460216040
I have an example where this rule is inconsistent when casting to the expected indexed type:
Playground link
What is it that is considered different about these that is preventing the first case? Note that this is occurring in 2.7 (see the playground link)
Why is this still not fixed?
I do think some change is certainly worthwhile, as this issue seems to extend to
Records. For example, ESLint recommends usingRecord<string, unknown>instead ofobjectas a type. However, interfaces by default are considered incompatible withRecord<string, unknown>, despite the fact that everything is supposed to be assignable tounknown.Though not quite what I wanted to hear, I think steve-taylor’s argumet earlier is valid. But if your object is expected to take virtually anything as a property’s value due to the
unknown, it doesn’t make sense for interfaces to be incompatible. (Extension is a workaround, but it doesn’t really make sense here.)Interestingly enough, interfaces are assignable to
Record<string, any>.@steve-taylor any thoughts on this particular scenario? I’d think making interfaces assignable to
Record<string, unknown>wouldn’t break OOP.Ran into this when trying to type a function argument that can have arbitrary keys but a known set of values. Unfortunately, the variable I was trying to pass into the function is generated from graphql-code-generator, so requires a bit more legwork than just changing an interface to a type manually. A quick workaround is to spread the variables before passing it in.
Playground
@RyanCavanaugh it wouldn’t make sense without also breaking polymorhism, which is a cornerstone of OOP. All sub-interfaces should be type compatible with their common base interface to enable polymorhism. Although an interface may be type compatible with an indexed interface by way of inference, it doesn’t necessarily mean that its sub-interfaces are type-compatible, because they may introduce additional properties of any type.
@RafaelSalguero consider the following interface:
It’s a well-established OOP convention that expressions of type
ThisIsWhyItDoesNotWorkcan be assigned where expressions of typedoesNotWorkcan be assigned. AlthoughdoesNotWorkis compatible with the index signature ofIndexType,ThisIsWhyItDoesNotWorkisn’t. TypeScript can’t determine at compile-time whether expressions of typedoesNotWorkare instances ofdoesNotWorkorThisIsWhyItDoesNotWork, so it can’t allowdoesNotWorkexpressions to be assigned whereIndexTypeis expected.You can fix this in one of three ways:
doWorksinstead ofdoesNotWorkdoesNotWork, allowing TypeScript to make the desired inference:IndexType:I vote to close this issue.
我已收到你的邮件
Something that can help considerably with this issue is to use the following type:
Now, if you encounter an issue similar to the one below, you will be able to get out of troubles:
This limitation makes it impossible to provide type definitions for a method that accepts anything JSON-serializable without bugging users to use workarounds:
https://www.typescriptlang.org/play?#code/C4TwDgpgBAUgygeQHIDUCGAbArtAvAKCigB8oBnYAJwEsA7Ac0JKlqwFsAjCSp0jge34YIaWrxZYMGcfGTpsEANoBdcQG8oisJX6RKoAFzkqdesqOzUmHFAC+Abnz4AJhADGGNJWgAzLLTdgan5aKGA0AGsIMksACi96C0QrBQBKIwA3fmpnR3wAenyoAD0AWlKoDGoOSi8QKDd+VwKi3Db2js6utpaoDPKoLDJuBqaIJzpgbh80N2g4SDdqTHkbNSYfQSN1oiJtXW5DCU5uRyIHfFsnVw8vaEbaCigEowX3ZYxViEdwqJjk+KUeipXq7MFlCrcHSUKAAC24EAANFAOFhgORYfxJM4oAB3fiUCJAA
my workaround for an error Type ‘ISomeInterface’ does not satisfy the constraint ‘Record<string, unknown>’. Index signature is missing in type ‘ISomeInterface’.(2344)
I used Omit utility type
Omit<ISomeInterface, never>What about this utility type?
Playground Link
I published this utility type in ts-indexify package.
You can extend types through intersection types. Is there anything I miss here?
Forever ago, @RyanCavanaugh said,
Could someone walk me through why declaration merging matters again? Imagine a world in which there were no errors in the following code:
Now someone comes along and merges something unexpected:
Wouldn’t this just produce errors inside
bar(), as expected?Isn’t that… good? I mean, people who want this feature presumably would be happy with new errors appearing if merges happen. Right now every attempted use of a non-index-signature-having interface in a place that needs an index signature is an error. I’d think the removal of these errors would be worth the potential of some of them coming back due to merging.
So I’m obviously not seeing the argument that declaration merging matters… could someone spell it out for me?
Gotta change dozens of typings from
interfacetotypebecause of this 😦Most people here seem to want to assign types like
{ id: number }to{ [key: string]: number }. But, if I am not mistaken, a value of type{ id: number }just isn’t a value of type{ [key: string]: number }. The second type demands a value for each key, but a{ id: number }only provides a value for the particular keyid.It seems to me that the proper solution to this is to introduce a new kind of index type; call it
FiniteRecord. An object of typeFiniteRecord<string, number>would have the constraint that allstringkeys that do exist on the object are associated withnumbervalues. However, no guarantee would be made that any keys actually exist.Then, for instance,
{ id: 15 }would be assignable toFiniteRecord<string, number>.FWIW, the type
FiniteRecord<K, V>is pretty similar toPartial<Record<K, V>>. However, partial records don’t solve the problem being discussed in this thread 😢How can I define a function that should receive an object where all the properties are
stringtypes?I thought I could do:
However due to this issue, this won’t work if the value I happen to pass in uses an interface type:
It’s easy to say that I should change the type definition from
interfacetotype, but if this comes from a third party, there’s not always the option to change the type definition. So if this issue is indeed intended behaviour, I presume there is a better and more correct way to definerequireObjectStringsso I don’t run into this limitation?我已收到你的邮件
This issue is old enough to be in kindergarten by now. Has no consensus been reached or has it just been abandoned?
@dzek69 I think you want just
.
https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgKLigT2QbwFDKHJjBgA2EAXMgM5hSgDmA3HgL554wCuICJAexDI4AE1Hp6mABQQMmapKwBKXMg55QkWIhQAhAQIDWuAkRLkqtek1ZER3MAAsBUanQYgWZwgAcGSNQg3AC2AEbQrBoIQnTIIZhhhkbUBsbIALym9hYU1ABEALJQAHTIAOJQEHL5ADQ+Ds6uBQBSAk7CACICEHUN-sCByACsw-VsrHhiEvLSCUnGyqxAA
That’s an issue for us as well
This is becoming problematic. A recent change in slonik made this issue resurface. It’s preventing ergonomic transitions to a more typesafe approach without breaking changes.
@acomagu From my understanding your argument is not so much about the specific behavior being discussed in this issue but against type widening, that is, allowing casts of concrete types into more generic ones. It is well understood that type widening worsens type safety but, nonetheless, typescript generally allows it, as it can be seen in the following code:
So, if this kind of type widening, which removes all type safety, is allowed, why is casting a
{ x: string }interface to{ [i: string]: string }not allowed (even though casting it toanyis allowed)? This leads us to the real problem that is caused by this behaviour: lack of consistency, a problem that manifests itself in three different fronts:typebut not on those declared usinginterface?unknown) insteadof the crypticIndex signature is missing in type?All in all, you could make the design decision that type widening is bad, ban it, and then apply that consistently, banning all direct casts to
any, or you could go and say that this case is an exception and then disallow type widening for bothinterfaceandtype-declared types in order to make this more consistent, or you could even keep the current behavior and allow wideningtypebut notinterface, but replace the error message with one that is clearer. Whatever you decide to do, it seems better than the status quo.I also encounter this pain. It is typically located at the boundary of the code: when sending data through API calls that just require plain JSON hashes (think of a GraphQL
variablesargument) but this argument is typed for soundness checking in the Typescript code.For now, I resort to the
{ ...x }trick of https://github.com/microsoft/TypeScript/issues/15300#issuecomment-436793742 but it looks hacky. It would be great to have an idiomatic way of handling this use case – especially because it encourages the good practice of typing more precisely.EDIT: Well, actually
x ? { ...x } : undefinedotherwisex === nulltransforms that to{}. And now you have to handlenull !== undefined😦EDIT 2: For now, resorted to using
types everywhere, and simulating interfaceextendswith&operator.I believe I have created a solution to this problem, which seems to work well.
Currently, I am working on a typescript library, which needs to take in some data (unknown shape),
JSON.stringifyit and send it on its way. My problem was that when you pass a Date object toJSON.stringify, it will create a string representation of that date. But when you parse the string back, it will keep it as a string. This also applies to instances of classes and similar things.I wanted users using my library to see an error when they pass an object with date property or something not JSON serializable. I googled a bit and found JSON type for typescript that works well. But I am running into this
index signature is missing...error too. I cannot force users of my library to not use interfaces, or to wrap everything they pass in with theDeepIndexSignaturegeneric as mentioned above.My solution is straight forward, although I am not sure if it will work for every use case.
You can see that I am able to pass in any object that matches the index signature, and it doesn’t throw an error. When I try to pass in the wrong object, it will throw an error. The last line of the error is index signature, but the line before states that
Type 'Date' is not assignable to type 'Json'which I think will have to be enough. It is not the best developer experience, but it does its job.Does anyone knows a hack from the library “perspective”?
Simple example, I want to create something like that:
So it’s allowing any shape object to be stored somewhere, the only requirement is for the entry is to have a string title.
Now users of my library may want to store books there:
I don’t want my users to be forced to use
TypifyorIndexSignatureHacktypes on their types. I’m seeking for a solution that will prevent this error and keep the library types working as intended.I don’t want my users to extend
Entryinterface as well, because from their perspective Book type should not accept any other fields that those three defined.Thanks @patrickneugebauer - I had noticed those rules too, and have resorted to using
typeoverinterfacein the common case, to allow building layers of type interactions without hitting walls of errors like these.In my example, changing the interface to a type does indeed get around the error.
There are some other differences between type aliases and interface that might affect your choice - namely you can
extendan interface but not a type alias. So it’s not quite that simple, but as rules of thumb go it’s not a bad one.Here’s how we get around this:
https://www.typescriptlang.org/play?ssl=1&ssc=1&pln=12&pc=1#code/JYWwDg9gTgLgBAbzgYQMqoApQmAprYXAZzgF84AzbEOAcilwEMBjGWgWACgvcAPSWHBgBPPHACSAOwAmfVMADmkxjACuDABIsA1gB4AKgD44AXkRwA2gGk4wSXG25hECnH0BdAFwSZcxcrVNHQNrd2NSLi4KVUlWYAh7CkkACggAIwArbyQLR2FvIhgoOwUvOBjtSQgAd3tSAEpvADcIYGlECO5OZgTCuELhABtcbylZXnklFXVcLWY9NExsPAJiYzMELjhKBJgAdVxFAAsYbwBWAAYLgBouTqiUgeH6riA
You can verify that there is still type safety by changing the font weight from
500to'500', for example.@acomagu I was referring to the mathematical definition of a transitive property, which, when applied to type casting would look like this:
Casting is transitive if, given three types A, B and C such that A can be cast to B and B can be cast to C, A can be cast to C.
I can see how my previous explanation can be confusing, but what I was trying to convey is that, when talking about casting in general (without specifying implicit or explicit casting) there’s already some cases in Typescript where that property is broken.
In any case, that’s an argument concerning the discussion on whether casting from
{ x: string }to{ [i: string]: string }should be allowed, whether that be through explicit or implicit casting (currently it’s impossible through either). Personally, I’m not sure what’s the best course of action here, as the problems that you’ve pointed out are very real but I can also see a need for this type of casts, so I don’t think I can add much to the discussion. What I feel more strongly about is the inconsistency of the current state, which we seem to agree about.@corollari The type
42 | 0is a generic type of42, but{ [i: string]: string }is not generic type of{ x: string }, I think. “A is a generic type of B” means “The all values acceptable for B can be also acceptable for A”, right? But regarding{ [i: string]: string }and{ x: string }, there is counterexample,{ x: 'str', y: 3 }.{ x: 'str', y: 3 }is acceptable for{ x: string }but not{ [i: string]: string }. Because the former type DON’T defines the properties other thanxbut the latter type demands the all properties should be string. So{ [i: string]: string }is not a generic type of{ x: string }.That’s why the casting between these types is different than
42 | 0and42, ornumber. It’s not widening.I’m not sure but I guess because interface can cause more complex problem like described at the my previous post, around
extends.Agree. More meaningful error message can be helpful for all.
Why is this not fixed yet? I have interfaces auto-generated by a third party library. Now how do I replace them with types, as some clever people here advise?
Seems related, currently there’s no way to enforce that object should have attribute values of specific type only.
Example: I want to define an abstract Table Row, basically any object limited to attribute values as primitive types. But it doesn’t work
@lgenzelis, ah yes, I adapted the utility type to be more robust by adding the conditional but forgot to update the playground. Without the conditional, it would fail, for example, passing it to React. See here. In that example, if you use the non-ternary version, then there will be type errors.
As for the infinite recursion, it seems TypeScript stops the recursion when it hits a primitive type (e.g.,
number,string, etc.)I’m seeing a lot of discussion that some type assignment is expected to display an error but I’m missing actual code that illustrates that issue. In my mind
seems fine even with module augmentation in mind. Especially considering
is apparently safe for TS.
Is there a fix in place for this or is this ruled out as a design choice? @SeaRyanC
There are instances where we want to infer interfaces to more generic types. The vast majority of libraries out there prefer to use
interfacethan types and in an instance where we’d like to infer them to more generic types, we can’t make the conversion.A lot of these implementations are classes implementing interfaces so I’m not sure exactly how to get the best of both worlds, that is: using interfaces for classes to implement while still using utilities that accept a more generic type-structure (where it’s compatible) of an interface.
@OliverJAsh @JemarJones you can do something like this:
Of course, this workaround is annoying since you can’t create a concrete type for it.
Maybe something like this will help you:
Update: there’s something wrong
I summed up a model to understand this situation:
I assume that each ts type can be described by two sets, called the accept set and the offer set, each element of both sets is a ts object literal.
A extends Bif the offer set of typeAis a subset of the accept set ofB.To describe these two sets, I created a small grammar, which I demonstrate below with some small examples:
examples
[]Represents a collection with no elements in it[{ }]Represents a collection with only one object:{}[{ x: 0 }]Represents a collection with only one object:{ x: 0 }[{ x: string }]Represents a collection containing all objects that satisfy the condition: there is only one keyx, value is a subclass ofstringContains:
{ x: '' },{ x: undefined }Does not contain:
{ x: 0 },{ x: '', y: '' }[{ (string)*: 0 }]Represents a collection containing all objects that satisfy the condition: there are zero or more keys, but keys must be subclasses ofstringContains:
{ x: 0 },{ x: 0, y: 0 },{ 0: 0 }({ '0': 0 }),{ }[{ (string - 'x' | 'y')*: 0 }]Represents a collection containing all objects that satisfy the condition: key must be a subclass ofstring, not'x' | 'y'[{ (keyof any - 'x')*: 0, x: 1 }]Represents a collection containing all objects that satisfy the condition: Must contain'x': 1. Values for keys other than'x'must be0I also summarized some common definitions:
common definitions
Now let’s go back to the topic. In the common definition above, the offer set
[{ x: 0 }]ofTXis a subset of the accept set ofII, whileIXis not. For example, the offer set ofIXcan contain{ y: 123 }, which is not a subset of the accept set ofII. HenceTX extends II, butIX extends IIdoes not hold.Note
The type of an object literal is the type of the corresponding type alias
The above contents are all summarized in my experiments, and there may be errors in them.
@younho9 Also for deep recursive objects:
Playground link
I would rather request expected behaviour to be like for interfaces. Otherwise, types can break runtime (we will have different types in TS and runtime)
Playground link
Code:
Sorry, might be little bit off-top, but i didn’t get why this type inference works:
I just found myself in a very similar situation to the one described by @mcpatten (had an interface wrapped in a Promise<> that I wanted to widen) where it’s not possible to use the spreading operator trick. For anybody that comes across this, the solution in this case is to use
typeinstead ofinterfaceto define your types, as it’s already been mentioned earlier in this thread.Rather than this:
Can you do this?
I’m not sure what part of your question is in the third party library, and what you control.
in case it helps anyone, I just found a workaround (essentially converting the
interfaceto atypeviaPick) for a slightly different situation where I hit the same underlying issue:and depending on the use case, I think it can be applied to the original issue:
^ Like it or not, implicit index signatures are an intended feature of TypeScript.
@RyanCavanaugh is there any news on this issue? I would have thought it would have been resolved by now as many more complex things have been built and released already.
Yes I know, sorry I meant that it should be assignable to
Partial<Record<string, number>>. The use cases that I want this for is when I want to accept any object with that shape, since I generally will be acting on it viaObject.entriesand what not.@acomagu That gives errors on the properties of
myBookinsideaddEntry: “Property ‘author’ does not exist on type ‘Entry’”@dzek69 Extending the
Entryshould solve the problem.See: https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgKLigT2QbwFDKHIDaA1hJgFzIDOYUoA5gLrUCuIpIA9gO4gBuAkTDAwAGwjU6DEIyEBfPHhgcEo7iGRwAJjvT1MACggYqaMwEpcwwqcMA6OGzAALblGQB6L8jCYABxQOLj4QBzwlPFBIWEQUACFublJkCAAPSBAdGgtDGyI-MUlpeiYhQuc3D1LZeVtkAIYkahA2AFsAI2hFZQRNOmR2zE7k0moklOQAXgKRYqlkACIAWSgHZABxKAhTJYAaBqr3KGolgCluVy0AEW4IA4am4BbkAFY3w4UhPF19MyMw1GKUsQiAA
EDIT: @dzek69 Just saw that you didn’t want the users to extend. In that case using a typeguard would also work.
See: https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgKLigT2QbwFDKHIDaA1hJgFzIDOYUoA5gLrUCuIpIA9gO4gBuAkTDAwAGwjU6DEIyEBfPHhgcEo7iGTAa6epgAUANzji2U5By58QASmomzKHWgzZ8RZFAhg2ULWCYAA4Q3DDIjubIALyxyADk3ABGAFYQ6vHCnoQAZDkRplEAhHEgbOLiWdl5CaISEPHaWpEQVZ41gSFhyMaFKHA0uMh1kuycPPzICrYAdCMosdEJMkyZnorKqiDqwJrIcAAmB3pYBhBu1Dgk5FS09Eys+yDY07hVwOEGOieG5-q2tje2SIfywczEkmQAHoocNgigVnIZm0QW4ZnA2GAABbcKDQ2GdFBWCYgZGeBTICDiGgoDzA4ZYqB8ZAgCC8NBQJlQAzxACSIEcwAOlLR8VsQnJeCUeFAkFgiBQACFuNxSECRBCLIj5FUMdjcdJ7nIJUQggwkNQygBbJLQDZ4BCaOjIK2YJIq0jUZWqmLqwjzagAIgAslAZsgAOLec6BgA0usxOKgQYAUtwsVoACLcCBxqpm4AW5AAVmL8YUQjwh2ObgMrvdqvFeCAA