TypeScript: 'type' aliases of string can not be used as object index signature.
type Guid = string;
var foo: { [deviceId: Guid]: DateTime } = {};
Generates the error:
An index signature parameter type must be ‘string’ or ‘number’.
It would be nice to be able to type indexes using string-aliases.
About this issue
- Original URL
- State: closed
- Created 9 years ago
- Reactions: 125
- Comments: 60 (12 by maintainers)
I use aliases to the string type to differentiate between different string types. I know they aren’t compile time enforced, but they act as useful documentation. Not being able to use them as object keys seems quite arbitrary and not in line with what a type alias is. Should I not be able to use an alias anywhere I could use the aliased type? If that is the design intent, I’d love to know what those situations are and why they were chosen. Thanks.
what is the point in aliasing
string
?From the TS documentation:
From the argument here:
I’m getting very mixed messages here. Why offer type aliases at all, then? Either you accept they’re a part of the language and their rules are implemented unilaterally throughout the language (that is, accept that type aliases are a feature), or you don’t.
The minute you do a bit of both is the minute you add a bunch of minefields to your software, minefields where I need to go, yet again, “why isn’t this rational thing I’d expect would work in TS not work for me in this instance but does in this instance?”
aleksey’s question is probably the most relevant.
This falls under the category of not appearing to do something you don’t. Specifically, all type aliases of a type are equivalent, so you could still index
foo
by aCustomerName
.There’s already a place to write the intended index semantics (
deviceId
should probably bedeviceGuid
?). Adding a second place to write that same information doesn’t help because it makes it look like the compiler will enforce the specific indexing type, when in reality it won’t.One nascent proposal at the moment is “Fresh types” which would allow an enforcement of the indexing type. If that ends up happening, we’d definitely revisit this.
I’ll respectfully ask the Typescript team to give this issue attention instead of ignoring users.
@orta, @DanielRosenwasser, @rbuckton, @sandersn, @sheetalkamat, @RyanCavanaugh
I’m cc’ing in recent contributors.
Please read this issue carefully and consider the facts:
I’d like due diligence to be applied to this feature. It’s a no brainer. There is a clear need for it, with no design pattern or workaround to compensate.
I’ve returned to this issue because I’ve run into this problem again and again and it’s driving me nuts.
@RyanCavanaugh
Whether it’s confusing is a matter of opinion (I personally don’t see how this would be any more confusing than the existing myriad of TypeScript functionalities), but it absolutely does lend a clear and useful function that to this day is sorely missing. Especially in a language like Javascript where it’s common to fire up objects and use them as key-value maps.
I have plenty of the above in my application. I use hashmap-like structures for performance and caching reasons. Each of those string indexes correspond to a particular property of another interface which I populate during initialization. I have no way of clearly indicating and tracing where those indexes would originate from. Other than having to write a clumsy comment which serves to bloat the code, or just having to remember what’s going on.
Notice how that is self-documenting, and how it objectively reduces confusion in this case by providing context? A person reading that not only understands that the index derives from a particular data structure, but they enjoy the fast-travel functionality in VSCode to speed up their workflow.
Why bring types to a language if you’re not going to properly leverage them?
Until a better solution is available, one alternative is to use JSDocs:
This will at least favor your future greps and offer a hint in your JSDocs / VSCode definition popups. You could also use the following syntax but I believe it only happens to work and is incorrect:
Agreed… PLEASE open this up… every time I have a custom string format (ISO8601 dates, email addresses, etc) I prefer to use a type alias rather than a raw “string”…
this is important to have!
@RyanCavanaugh I request that this issue is reopened, because I’m still running into this issue, you haven’t suggested any workarounds to this issue, and have offered a rather unconvincing justification that seems to be skirting the reality of the problem.
Please clearly read this:
https://github.com/microsoft/TypeScript/issues/1778#issuecomment-586419491
And tell me how I’m supposed to be handling this scenario beyond an argument like ‘you’re holding it wrong’.
I know I’m not writing your paycheck but this is clearly an ongoing issue and I get the impression your team is sweeping it under the rug favouring semantics over actual developer concerns, and this is becoming immensely frustrating to read.
Please at least do me the service of responding to what is being said here. If I (among others) are just shouting into the wind, let us know your decision is final and not for any more debate, and I will divest myself and move on.
I don’t get it, there’s a clear benefit here.
@RyanCavanaugh The feature is that context can be given to what that index is supposed to be used for. To me it seems arbitrary to prevent this from happening, you’re restricting the amount of self-documenting code that can be written, with zero apparent drawbacks.
People are now coming up with weird workarounds to emulate this, so it has a clear need.
Restricting the type alias to allow only certain primitives is a perfectly fine caveat.
Maps allow generics to both provide type safety and improve code readability, why wouldn’t you treat index signatures with the same amount of consideration?
I’d ask that this is reopened and given a proper look. Look at the votes.
You wrote this comment and I’m reading it, same as we read all the other comments. What specific behavior are you looking for from us to make you feel that your opinion is able to be expressed (in the context that we do still believe this isn’t a good fit for the language)?
It doesn’t offer any features at all - that’s the problem!
As a complete surprise, this just started working.
Digging deeper, the new index signature features are in the 4.4 release notes.
I don’t know what I can do that will make people happy except to implement a feature we think is a bad idea. We’ve read the feedback, we understand it, we see why people might want it, but we think the feature is a bad idea to add to the language at this time and are thus choosing not to do so. Someone not agreeing with you is not the same as them not listening to you.
It all boils down to:
We think allowing you to write type aliases here creates the implication that you’re making different kinds of index signature, but you’re not. It’s actively confusing and doesn’t create any new functionality. Why would we add new syntax for doing something you can already do with perfect clarity? It’s harmful.
Using type aliases over raw primitive types is useful especially in big systems with heavy domain logic.
Example usecase: a bookkeeping system or e-commerce system. First you start with a local currency with
number
. But after the system grows big and have lots of customers and you need to replacenumber
with{ amount: number, currency: Currency }
in few hundred places, then good luck. Instead, you start withtype Money = number
, useMoney
everywhere. And when you need to change the entity, you’ll get precise list of all places where compiler finds an error, the ones that you need to update (with primitivenumber
you would get errors as well, but not in the places you need to update, less convenient).It’s basically a Value Object in DDD. TS Aliases are superb with it.
This +1000. Either types can be aliased or they can’t. Either
'foo' | 'bar'
are a limited set of strings or they aren’t. This weird thing we got here where it’s like “the key is assigned a type, except it isn’t, becausestring
isn’t a type the way types are types” is contrary to the rest of the language.All the prior input we’ve provided is still true
Due to unresponsiveness on this issue, I’m raising another one:
https://github.com/microsoft/TypeScript/issues/37448
I sometimes use aliases the same way as @Yona-Appletree does. When I work with Ethereum’s addresses I create alias
export type Address = string
. I understand @aleksey-bykov’s point, but why should the case of object’s index differ from using an alias in for example function’s parameter? e.g(myParam: Address) => myParam
is completely fine. I don’t see the point of explicitly forbidding of using aliases in object’s index.Could we mb reopen this issue, please?
Is there any other place where type aliases can’t be used? That is what has always confused me. This has never seemed like a new feature but a missing application of the existing feature: type aliases. The very first line of the code example for type aliases in the handbook is an example of branded strings (https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases), so there’s no indication that their use is discouraged. Of anywhere, branded strings for index signatures is the place where that makes the most sense to expect that people will careful about whether they are really just strings, rather than aliases used in function signatures or property types. Why is there less concern about people expecting compile-time safety with branded strings when used anywhere else?
I’m honestly looking for an example. I’ve been curious about this while getting periodic notifications on this issue over the last 5 years. I can’t think of a reason for type aliases not to be usable in 100% of the same places as regular types.
didn’t mean to play a devil’s advocate, aliasing a string might be helpful at early prototyping stages where, say, ID isn’t yet clear to be a string or a number, using an alias to either can reduce refactoring effort, but this use case is too dubious to get enough justification
if people think that aliasing would create a new type incompatible with the original one, so they can benefit from such distinction, it’s a dangerous fallacy and illusion (need nominate types to do so) On Jan 4, 2016 7:01 PM, “Ryan Cavanaugh” notifications@github.com wrote:
@RyanCavanaugh
IMO that’s false, in that if you can limit keys to a particular set of strings, you can do union types in which the types aren’t in conflict. e.g. if you do
[key: string]: number
then all other keys / types must conform to a number. But if you do[key: Foo]: number
, then ONLY keys conforming toFoo
must be a type ofnumber
(or a compatible type such asany
).AFAIK, there’s no way to specify “defined keys are these types, any extra keys are of this specific type” using current syntax. A simple solution would be to limit extra keys to a specific set of strings.
I was also trying to use this as self-documenting code and found I couldn’t. As others, I had to introduce awkward comments to describe what the key is, instead. Not ideal.
For what it’s worth, I’ve started writing object key name and key type backwards. Obviously this is not safe if the definitions change but at least it let’s you document the intended type of the key.
I would also like to add my support for this feature. The arguments seem to be that this doesn’t add any features, and there doesn’t seem to be any reason to type alias to primitives because it makes no different to compiler. However given that there already is support for type aliasing primitives, it seems like there should be consistency as to where these aliases could be used. Type aliases for strings can and are used to make code far more self documenting, take the (exaggerated) example:
vs
I think there’s a stark difference between the clarity of typing, despite both being equal at compile time. Ironically, TS currently allows for an “in-between” solution, where you can type alias to strings but can’t use those type aliases to define key types.
Like @niedzielski mentionde, the following syntax
actually works, and
Foo
gets resolved to{[x: number]: any}
as expected! I also believe this is just a happenstance of how mapped types work, since{[K in number}: any}
is also valid. However, doesn’t mean we can’t use it! 😉This isn’t very convincing because it feels like arguing for Hungarian notation.
It seems rational that indexes wouldn’t be compiler enforced because anything type aliased isn’t enforced. That is, my original example with
Guid
is as strongly typed as this follow valid code is:I see no reason that this permits a string to be passed to
someFunction
, butSomeNewType
can’t be used as an index.I want to lend my support for this feature as well. My company has a lot of interfaces representing domain objects that are loaded in from the backend. Sometimes we need to create other data structures that use the data from those domain objects in various ways, including as the keys of objects. Here’s a somewhat silly example: an object in which the keys are student IDs and the value is the status of the student’s homework.
It would be really nice to be able to rewrite this without the comment and leverage the student interface we already have:
Why is this better?
@RyanCavanaugh What do you (and other TS folk) think about these two benefits?
There is a work-around we’re thinking about using to get type-enforcement of different categories of strings (e.g. ids for different types of objects), and to track those properly through being used as object keys, which is to declare them (inaccurately) as string literals, since those are the only types that can be used productively as index types in an object.
to create them, you just need to cast a string (either directly, or wrapped in a constructor-like function:
The type system behavior is quite sane for this use case, and you can do things like:
and get type safety against accidental switches for both reads and writes for keys and values, including fairly readable error messages (checking is the best with noImplicitAny enabled).
And they can also be used directly as strings, so it has no impact on interpolation, serialization, etc.
I’d be very interested in knowing if anyone else has tried this approach and knows if this is a sane path, or if there is some reason this will be a bad idea (Other than the obvious, that this isn’t safe against use of constant strings which happen to match the specific strings, which we’ve decided is less of a problem than the problem this is solving).
String
is not an alias forstring
. It refers to the non-primitive typeString
in JavaScript.Can we get some input from maintainers on this? It’s sorely missed and difficult to find an open source solution without it.
Another reason for this is to define keys as not belonging to other keys, like:
The only way there appears to do this now is some kind of really complex union types like: the answer in this Stack Overflow post: https://stackoverflow.com/questions/49969390/how-do-i-type-an-object-with-known-and-unknown-keys-in-typescript/57993115#57993115
@RyanCavanaugh could this be revisited and could there be a proper discussion on the topic? I feel like the amount of people voting for this feature proposal are in the majority and no chance has been given to us to express why we’d want this. Even if there is no enforcement yet, is there any chance for limited support for this and not errors generated by the compiler. Because in your own words:
Then why is it disallowed? What would in your opinion be the downsides of still implementing this behaviour? Does it outweigh the features it offers? Are these restrictions of Javascript even possible to be worked around? And if not, shouldn’t we accept these restrictions and still move forward with these limited features?
I cry every time I hit this arbitrary limitation.
Also see https://github.com/microsoft/TypeScript/issues/15746 which got closed.
I was playing around with branded strings for extra type safety. I’ve never played with branded types much before.
Then, I noticed this annoying thing,
So, I made a workaround,
Playground
I really wish branded strings could be used to index objects. But I’ll take this workaround.
[Edit]
You might need to change the parameter to
Readonly<Record<K, V>>
, if you’re working with readonly records.@nwshane It works 😄
You just have to use
in
in the declaration of the index signature:It even gives you code completition See here
For me it was a surprise that it doesn’t work in the old versions of the TS. To support old TS versions I had to change my Guid type alias to string. Many thanks to all those people who requested this feature for so many years! It’s a win! 😃
@RyanCavanaugh this is an issue of ergonomics and there is clearly an issue. What do you propose to offset the workarounds we’ve been doing?
Seems like a legit requirement. Flow handles this just fine.
I’m hitting the same problem in JXA (JavaScript for Automation, in macOS), when a collection is of named objects. Consider,
In other words,
Application("Finder").windows
is an object where every string key is a Window, except specifically forbyName
—and similarlybyId
andat
—which are all functions. There appears to be no way to type this object in TypeScript.@ducin That’s an excellent point!
It would seem you also can’t pass aliased types to an indexer either:
An index expression argument must be of type 'string', 'number', 'symbol, or 'any'
And yes, there is a missing
'
in the error message.@matthew-dean But this issue is ONLY about allowing an alias instead of
string
(or presumablynumber
). Is specifically not about allowing index signatures with unions.Allowing unions would be a different matter and is the subject of this (presumably on the back burner) PR #26797. Without the abilities in #26797 I don’t think allowing aliases in that position really brings much value.
jfc, i was just talking about your example with Currency and Money, didn’t say or mean it can/should be used anywhere