TypeScript Version: 2.2.2
Code
function getName(id: number) {
return {
1: 'one',
2: 'two',
}[id] || 'something else';
}
Expected behavior:
{1: 'one'}[id]
type should be string | undefined
So I would expect the return type to be inferred as string.
Actual behavior:
Element implicitly has an 'any' type because type '{ 1: string; 2: string; }' has no index signature.
Interesting point -- a fresh object literal type certainly could have an index signature derived from its properties. Not sure how common this pattern is in practice, though (do you really want to allocate a new object every time this function gets called?).
The function is to illustrate my point. I found this pattern more elegant than a switch/case for example. Is it as performant? I don't know but I don't think I care most of the time.
Are you suggesting that it could work only if [id]
is applied directly to the object declaration? Ideally, I'd like to also be able to do:
const nameMap = {
1: 'one',
2: 'two',
};
function getName(id: number) { // inferred return type should be string
return nameMap[id] || 'something else';
}
The implicit any there is by design -- we don't allow arbitrary indexing into objects that don't include an index signature in their type. Once an object has been "indirected" we no longer trust that it isn't an alias to some other object that might have non-string properties (in this case).
Ok, makes sense. Supporting my first example would already be nice for me.
I'd go so far as to expect the return type from the first example to be 'one' | 'two' | 'something else'
. At least, the only reason I would use that particular pattern would be if that were my desired return type. Otherwise the easy solution is to do:
const nameMap: { [key: number]: string } = {
1: 'one',
2: 'two'
};
function getName(id: number) { // inferred return type is string
return nameMap[id] || 'something else';
}
@PyroVortex we have no way to know that the line nameMap[3] = "bob";
isn't somewhere in your program
@RyanCavanaugh
I was specifically concerned with the pattern
return {
1: 'one',
2: 'two'
}[id];
for which the object is for all intents and purposes completely immutable by virtue of scoping. With this particular construction I would want the compiler to be as specific as possible in the typing. Otherwise I would fall back to the pattern of specifying a type for the constant.
:+1: for this feature. Lack of the index signature often makes auto-inherited types useless, so I need to copy-paste type inherited by typescript compiler and add index signature to it.
The fix is really simple and clear in my opinion - for objects like this:
const randomMapping = {
stringProp: "1",
numberProp: 2
};
Instead of the following type:
interface RandomMappingWithoutIndexSignature {
stringProp: string;
numberProp: number;
}
Generate type with the index signature:
interface RandomMappingWithIndexSignature {
stringProp: string;
numberProp: number;
[propName: string]: string | number | undefined;
}
Am I missing something?
We have a use case where this would be nice as well. The simple Knex.js setup code below won't work without declaring the type of configs
; I just want to index in and get whatever config obj matches the env
.
const env = process.env.NODE_ENV || 'development';
const configs = { development, test, production };
const knex = knexConstructor(configs[env]);
However in the case @vladimir-tikhonov shows, I think that index signature can't be reasonably inferred. Even if it's const
, an object can still have properties added later returning any arbitrary type. Indexing into the object would then have to return any
- and that's an implicit any.
So it seems to me the design @RyanCavanaugh is referring to is correct and inevitable - there's no way to guarantee you're going to get specific types (i.e. not any
) from the index signature.
Also allow union types for keys please but I do believe that some sort of inference could be done here
I'm not sure if this is the same issue, but I'm getting a similar Error:
TS7017: Element implicitly has an 'any' type because type 'iAbilityModifiers' has no index signature.
Here's a simplified example:
enum eFoobar {
foo = 'foo',
bar = 'bar',
}
type tFooBar = { // this is `iAbilityModifiers` from the error
[fb in keyof eFoobar]: number;
};
```ts
const character = {
// …
foobars: _.reduce(eFoobar,
(acc, key) => _.merge(acc, {[key]: 10}),
{} as tFooBar
),
// …
},
```jsx
{_.map(someOptions, ({ text: foobar, value: key }) => (
<input value={character.foobars[key]} /> {/* error references this line */}
)}
According to the Mapped Types documentation, adding keyof
in [fb in keyof eFoobar]
is supposed to take care of the typing:
In these examples, the properties list is
keyof T
and the resulting type is some variant ofT[P]
. This is a good template for any general use of mapped types. That’s because this kind of transformation is homomorphic, which means that the mapping applies only to properties ofT
and no others.
Not sure if this is helpful in terms of adding a use-case, but I encountered what appears to be the same (or related?) issue by using a type annotation that uses an enum
to restrict the names of properties. This is accomplished by defining the index signature (I think? I'm honestly not sure about the terminology here - "index signature" seems to be something very specific to TypeScript...?):
type EnumIndexedType =
{
// Note: entries are optional.
[TKey in SomeList]?: string; // <-- is this not an index signature?
}
See the full example at the TypeScript playground here (issue on line 27).
If someone can point out a better or otherwise "more correct" way of doing what I'm attempting here that works as TypeScript expects, I'd very much appreciate it!
Regardless, the error as it stands is very confusing: "_No Index Signature?! Isn't that, like, the _only_ type annotation I wrote?_"
@ericdrobinson, yes, the error message is extremely confusing. However, what it is actually complaining about is not the string
.
What you wrote is called a Mapped Type (horrible name for trying to google it).
You've got 2 choices:
type Hashmap<K, V> = {
[k in K]: V;
}
I usually use this with K
being an enum and V
being some kind of class/interface:
enum eCharacterClass {
mage = 'mage',
rogue = 'rogue',
warrior = 'warrior',
}
class CharacterClass {
health: number = 1;
// …
}
type tCharacterClassSet = HashMap<eCharacterClass, CharacterClass>;
/*
{
[eCharacterClass.mage]: CharacterClass{},
[eCharacterClass.rogue]: CharacterClass{},
[eCharacterClass.warrior]: CharacterClass{},
}
*/
So you could do:
type EnumIndexedType = Hashmap<SomeList, string>;
Alternatively, you could create a weak relationship:
interface WeakHashmap<V> {
[key: string]: V[keyof V];
}
type tCharacterClassWeakSet = WeakHashmap<CharacterClass>;
/*
{
mage: CharacterClass{},
rogue: CharacterClass{},
warrior: CharacterClass{},
}
*/
In this second one, the "index signature" will be string, which means there is a weak link between your Mapped Type and your enum: You can bypass the enum by supplying any value that happens to be in it ('foo'
instead of SomeList.foo
).
@jshado1 Your Option 2 makes total sense to me but doesn't provide the same restrictions I'm going for, as you point out. With Option 1, I'm not able to get things to work in my actual environment.
First, in order to remove errors, I had to modify the HashMap
definition to this:
type HashMap<K extends string, V> = {
[k in K]: V;
}
Notice the extends string
. That removed the error stating that
Type 'K' is not assignable to type 'string'.
At the end of the day, the HashMap
version _also_ resulted in the same type of error in VSCode:
[ts] Element implicitly has an 'any' type because type 'HashMap
' has no index signature.
To be clear, viewing the issue in my previous comment requires enabling the noImplicitAny option in the Options dropdown. Indeed, the code suggested in Option 2 appears to result in the same error when the noImplicitAny option is set. Please see this merged example and enable the noImplicitAny option to see the issue.
@ericdrobinson are you using an enum with string values? I just tried it without, and I get that error, but when I switch it to the string enum, it works (Playground). You cannot cast an enum (a collection of options) to a string, but you can cast its options to strings if need-be. I don't know the TS syntax for doing that in a Mapped Type (I'd guess either as string
or : string
somewhere).
are you using an enum with string values?
@jshado1 I take it you didn't actually look at the Playground example I provided and follow the directions about setting the noImplicitAny option, yes? The enum I used does indeed have string
values.
I have adjusted the Playground example that you provided to include an actual triggering use of the issue. Instructions:
You should see an error appear no line 27, one built from your code.
Line 34, on the other hand, has a workaround. As the comment above the for-in loop suggests, the error is confusing and does not suggest that the workaround is to explicitly cast the type of entry
to the enum type, something that TypeScript should theoretically be able to determine. At the very least, TypeScript should identify index signatures built with enums as valid (at least when string values are supplied as in the provided examples).
The error message is truly confusing - I just spend good few hours before looking elsewhere before realizing this...
(TS 2.8 / 2.9@rc)
const palette = {
primary: 'hotpink',
secondary: 'green',
danger: 'red'
}
type PaletteColors = keyof typeof palette
type Palette = { [Color in PaletteColors]: string }
// Not really necessary but let's assert index signature second time
const myPalette = palette as Palette
const key = 'primarrrrry'
// ERR: Element implicitly has an 'any' type because type Palette
// has no index signature.
const dynamic = myPalette[key]
The Error here is, that var key
is not assignable to index type/ index signature of Palette
. In my example Palette
has index signature (from object literal expression and another one from type assertion)
Did I get it right? - If yes, then this error message should definitively be tweaked, because it's plain wrong, not just confusing...
The Error here is, that var key is not assignable to index type/ index signature of Palette. In my example Palette has index signature (from object literal expression and another one from type assertion)
If you look at the type of myPalette
in the Playground, you'll see that it's type is the literal object rather than something that resolved to Palette
. If you annotate myPalette
to be of type Palette
then you _still_ get the "no index signature" error. Which... is confusing because Palette
technically _does_ have an index signature (unless I'm missing something - apparently TypeScript index signatures require that the index key be of type string
, number
, or Symbol so perhaps something there).
To me, this part feels like a bug.
That said, the error you encounter drops away entirely if you add an explicit type declaration to your definition of key
:
const key: PaletteColors = 'primarry'
Rather, what you get _now_ is a far more helpful error that says:
Type '"primarry"' is not assignable to type '"primary" | "secondary" | "danger"'.
At the end of the day, more explicit declaration of expected types appears to appease the compiler. However, the compiler's current error is extremely confusing as both you (@vadistic) and myself have encountered.
Yes, TypeScript is very prone to throwing "has no index signature" whenever the wind blows (it seems to be the TS version of "something went wrong").
@ericdrobinson sorry before, I missed the enable noImplicitAny
step (I thought it was already enabled). I use those two Mapped Types in my own project, which has noImplicitAny
enabled, and they don't cause that error.
Which... is confusing because Palette technically does have an index signature
Palette
is a mapped type, and mapped types don't have index signatures. The fact that both use [
]
is a syntactic coincidence.
In my example Palette has index signature (from object literal expression and another one from type assertion)
Object literals don't have index signatures. They are assignable to types with index signatures if they have compatible properties and are fresh (i.e. provably do not have properties we don't know about) but never have index signatures implicitly or explicitly.
Nothing in this example has an index signature.
@RyanCavanaugh @ericdrobinson Thanks for answering.
I thought I knew how they worked, but it looks I was wrong and made two false assumptions:
1) Thought that typeof of object literals are inferred to have (string | number) index signature
2) Thought that mapped types over union of string literals is a way to declare a limited set of string index signatures That's how it looked to me from the example in Typescript Deep Dive
I'll think over it, thanks!
Interesting point -- a fresh object literal type certainly could have an index signature derived from its properties. Not sure how common this pattern is in practice, though (do you really want to allocate a new object every time this function gets called?).
@RyanCavanaugh I ran across this when using the JavaScript for-in
pattern with a [mapped] type that used enum values as indices. However, it _also_ affects a far simpler (and likely more common) case:
const palette = {
primary: 'hotpink',
secondary: 'green',
danger: 'red'
}
for (let elt in palette)
{
console.log(palette[elt]); // ... type {...} has no index signature.
}
Here is a playground showcasing the issue. Be sure to turn on the noImplicitAny
option.
This is the standard affair use-case for the for-in
pattern. Check out the example used on MDN for the for...in
statement:
var string1 = "";
var object1 = {a: 1, b: 2, c: 3};
for (var property1 in object1) {
string1 = string1 + object1[property1];
}
console.log(string1);
Copy the MDN example over into the TypeScript playground and you get the same error (with the noImplicitAny
option enabled, of course).
The enum case that lead me here is simply a variation on this theme.
Given that this is a standard method for iterating over properties of an object in JavaScript (and theoretically TypeScript), what is the TypeScript Way™ of handling the implicit any that appears due to object literals not having a standard index signature?
Palette
is a mapped type, and mapped types don't have index signatures. The fact that both use[ ]
is a syntactic coincidence.
@RyanCavanaugh If []
refers to index signatures with concrete types, what do you call []
when used in a mapped types definition? Is there a term?
That aside, I see now that what the []
is doing in the mapped type is syntactic sugar to encapsulate a set of "types" (not sure of the correct terminology here). When you _expand_ what is inside the []
, you end up with a list of concrete properties (I assume the compiler/language service does this in the background). That syntax effectively encapsulates the mapping rules.
@ericdrobinson Regarding the first example, we have to put aside the fact that we (from inspection) can see the runtime behavior of this code by executing it in our heads. If palette
, which has the type {primary: string, secondary: string, danger: string}
came from somewhere else, it might be an alias to an object that actually had the runtime shape {primary: string, secondary: string, danger: string, OTHER: number}
. So we can't guarantee that palette[elt]
always produces a string
.
Of course, we can always add more special cases to the type system to detect the specific case of iterating over a known-bounded object literal, but this just leads to the "Why does this code not behave like this other nearly-identical code?" problem when we fail to properly exhaust all the special cases.
The "standard" way would be to write const palette: { [key: string]: string }
= {...`
Regarding mapped types, remember that { [K in T]: U }
is a special form - it can't be combined with other properties within the { }
. So there's not really a name for the [K in T: U]
part because it's just part of the overall "mapped type" syntax { [K in T]: U }
Regarding the first example, we have to put aside the fact that we (from inspection) can see the runtime behavior of this code by executing it in our heads. If
palette
, which has the type{primary: string, secondary: string, danger: string}
came _from somewhere else_, it might be an alias to an object that actually had the runtime shape _{primary: string, secondary: string, danger: string, OTHER: number}_. So we can't guarantee that _palette[elt]_ always produces a string.
@RyanCavanaugh Fair enough! That certainly is a fair point. I considered adding a default [key: string]: any
(and key: number
equivalent) to object literals but that would reduce the ability of a user to create something more specific without jumping through hoops. Hmm...
Perhaps consider adjusting the error to be more informative? Looking for info on this (while admittedly being under the false assumption that the Mapped Type was a restrictive index signature of sorts) didn't turn up much helpful information.
Alternatively (or in addition), could you expand upon index signatures in the documentation to provide an explanation as to why they're not provided by default (especially with object literals)? I'd guess I'm not the only one to be tripped up by the []
syntax of mapped types.
The "standard" way would be to write
const palette: { [key: string]: string } = {...
Mind blown.
Regarding mapped types, remember that
{ [K in T]: U }
is a special form - it can't be combined with other properties within the{ }
. So there's not really a name for the[K in T: U]
part because it's just part of the overall "mapped type" syntax{ [K in T]: U }
.
Got it. I will say that when I tested out the "standard" way you suggested above, I immediately found myself trying to adjust it to const palette: { [key in SomeEnum]: string } = {...
. It only occurred to me after seeing the implicit any error return that I had merely created an inline mapped type... that syntactic coincidence strikes again... ;p
Here's my little example that caused me to run into this issue (mostly typed in IDE to reduce syntax errors):
type UnionParameter = object | string | boolean;
export class MyLittleExample {
public getBooleanAttributeKeysIfApplicable(unionParameter: UnionParameter): Array<string> {
const justTheBooleanKeys = [];
if (typeof unionParameter === 'object') {
const unionParameterKeys = Object.keys(unionParameter);
for (const key of unionParameterKeys) {
if (typeof unionParameter[key] === 'boolean') { // ran into 'has no index signature here
justTheBooleanKeys.push(key);
}
}
}
return justTheBooleanKeys;
}
}
Based on other comments, I sort of see why this is happening. However, unless my feeble brain is feeble, this looks like a valid use case and I might not be alone.
I think the problem is two-fold.
Consider the following example:
const obj = {
a: 1,
b: 2
}
for (val key in obj) {
console.log(obj[key])
}
This results in the following error:
Element implicitly has an 'any' type because type '{ a: number; b: number; }' has no index signature.
Note that key
is considered to be string
. Where-as we can see that key
could be "a" | "b"
in this example. The error makes sense when key
is string
, as the set of keys is higher than the keys that are in the object, thus indexing is dangerous. However, when key
would have been considered to be "a" | "b"
then the error shouldn't show up.
I also think that when the indexer type is greater than the key type (string
vs "a" | "b"
), then Typescript might want to infer | undefined
from the result of the indexer.
So, Typescript is too quick to derive string
from an object key. In addition, Typescript shouldn't error when the key of the indexer exactly matches the key type of the object.
Accepting PRs for a solution that special-cases property access expressions where the thing being accessed is an object literal. Use the type of the index to filter to matching properties (the relevant indexing type here could be a string, number, or union of literals thereof) and return a union of the matching property types along with undefined
if the indexing type is not bounded to only matching literals.
It looks like there may be a fix in for this? But in the mean time I'm going to post a suggestion for others who came here like me. My use case was I had a mapped type that I could use when I had a bunch of identical reducers for different namespaces/domains (just a combo of Record and Readonly) and it worked in most cases but I ran into an issue where it gave me an implicit any
error when I tried something like somethingReducer[domain]
. I found that I could make a second type that just defined an index signature, and then use that in connection with my mapped type to fix the error. essentially you just have to go from this:
declare type DomainMap<DomainEnum extends string, Type> = {
readonly [value in DomainEnum]: Type;
}
to this:
declare type DefineIndexSignature<Type> = {
[key: string]: Type
}
declare type DomainMap<DomainEnum extends string, Type> = DefineIndexSignature<Type> & {
readonly [value in DomainEnum]: Type;
}
hope that might help others who have my same issue . . .
I entended the color pallete exercise. May be of interest to some.
Playground - TURN ON STRICT MODE
const palette = {
primary: { color: 'hotpink' },
secondary: { color: 'green' },
danger: { color: 'red', sound: 'alert' }
}
// the named entries: 'primary' | 'secondary' | 'danger'
type PaletteColor = keyof typeof palette
// infer the whole type of the pallete
// (not a string based index)
type Palette = typeof palette;
// just to prove we really have the 'Palette' type here I create a forced reference
const fakePalette: Palette = <any> undefined;
// via 'Palette' we now have the FULL tree of our palette object available
// THIS FAILS AT RUNTIME OBVIOUSLY BUT THIS SHOWS COMPILER UNDERSTANDS IT
fakePalette.danger.color;
fakePalette.danger.sound;
// but not this one
fakePalette.primary.sound; // doesn't exist
// now back to dynamic strings
// make a reference to the palette (no particular reason!)
const myPalette = palette; // don't need to assign a type here
// create a key somehow. compile definitely doesn't think this is a color
const key = 'prim' + 'ary';
// cast it to 'PaletteColor'
// caution: this could obviously now give runtime errors
const dynamic = myPalette[key as PaletteColor]
// try to get some properties from our 'dynamic' color pallette
const dynamicColor = dynamic.color; // runtime gives 'hotpink' - this works!
console.log('dynamic.color: ' + dynamicColor);
// only common properties to all palette entries are available now
const dynamicCikir = dynamic.color; // common to all
const dynamicSound = dynamic.sound; // only in 'danger'
// hover over this
type typeOfDynamic = typeof dynamic;
// you can't call 'sound' or even check for its existance (without type guard)
if (dynamic.sound)
{
// isn't on all entries
}
// the only way I can see to fix this is to define an interface for the entries
interface PalletteEntry { color: string, sound?: 'alert' | 'barking' };
// and force it on each one
const palette2 = {
primary: <PalletteEntry> { color: 'hotpink' },
secondary: <PalletteEntry> { color: 'green' },
danger: <PalletteEntry> { color: 'red', sound: 'alert' }
}
// now every palette entry has 'color' and 'sound' available
// even if they don't actually exist
palette2.danger.color;
palette2.danger.sound;
palette2.secondary.sound; // 'alert' | 'barking' | undefined
// the alternative is to make this a string indexed type
// but then you lose the ability to just type this anywhere
palette.primary
Addendum: You can make an 'inline' typeguard like this (in the situation that not all properties on your 'type' are of the same exact type)
if ('sound' in dynamic)
{
dynamic.sound;
}
Palette
is a mapped type, and mapped types don't have index signatures. The fact that both use[
]
is a syntactic coincidence.
This was the "aha" moment in this thread which made the error message make sense. It would be really helpful if the error message when doing index access on a mapped type explained this, or if the mapped type documentation mentioned this.
It would be even better if indexing over mapped types would produce the value, which i think is what #29528 is asking for.
@RyanCavanaugh I'm a bit confused here. This particular error is frequent and annoying for me, but this example shouldn't be an exception.
a fresh object literal type certainly could have an index signature derived from its properties.
The fact that the object is static is irrelevant: the problem here is the key is a plain number
and not keyof staticObject
The correct type should be:
function getName(id: 1 | 2) {
return {
1: 'one',
2: 'two',
}[id] || 'something else';
}
Valid: https://www.typescriptlang.org/play/#code/GYVwdgxgLglg9mABAcwKZQHIEMC2qAUMAJgFyICMiAPogEwCUZAzlAE4xjKIDeAUIolboQrJHwEDyZAOQJU0gDT8JtGVADucRcoC+AbWIBdajWlM4eKAAsOXVABsm8gNy8dQA
Fix/improve the Type has no index signature
error in general, don't make this an exception.
@fregante I'm not really sure what you're getting at
Accepting PRs for a solution that special-cases property access expressions where the thing being accessed is an object literal.
Maybe I misread your solution proposal. Did you say that in this case only the error “Type has no index signature” will be dropped? Because I don’t think this is a special case.
However if my example (which doesn’t cause the error) returns 'one' | 'two' | 'something else'
instead of string
then that’s good.
@RyanCavanaugh
Hi, I submitted the PR https://github.com/microsoft/TypeScript/pull/37903, could you take a look at it?
Most helpful comment
I'd go so far as to expect the return type from the first example to be
'one' | 'two' | 'something else'
. At least, the only reason I would use that particular pattern would be if that were my desired return type. Otherwise the easy solution is to do: