-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
'type' aliases of string can not be used as object index signature. #1778
Comments
+1 |
This includes the alias class Map<T> {
[key: String]: T
} |
It would seem you also can't pass aliased types to an indexer either: class Map<T> {
[key: string]: T
}
let map: Map<number> = ...
let key: String = ...
let value = map[key]; // error
And yes, there is a missing |
|
Ah I didn't know that, thanks! I suppose this means I should be using |
what is the point in aliasing |
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 There's already a place to write the intended index semantics ( 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. |
didn't mean to play a devil's advocate, aliasing a string might be helpful if people think that aliasing would create a new type incompatible with the
|
That would indeed be more inline with what I'm after. Thank you for the update. |
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. |
Until a better solution is available, one alternative is to use JSDocs: type Guid = string;
/** @type {Object.<Guid, DateTime>} */
var foo: { [deviceId: string]: DateTime } = {}; 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: type Guid = string;
var foo: { [deviceId in Guid]: DateTime } = {};
foo.bar = new DateTime(); |
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). |
I sometimes use aliases the same way as @Yona-Appletree does. When I work with Ethereum's addresses I create alias Could we mb reopen this issue, please? |
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! |
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 It's basically a Value Object in DDD. TS Aliases are superb with it. |
@ducin That's an excellent point! |
@Aleksey-Bykov why "better" if DDD is a well-established and commonly used approach? |
because if we follow your example it lets you differentiate Money further by assigning a unit of measure to them (think dollars or euro) making them inmediately incompatible YET convertable to one another using just the arithmetic |
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. |
Just to give another real life example, where TypeScript fails to type a JS object, I'm using this pattern often in the next version of Less (simplified for this example). type Props = {
value: string | number
text: string
}
type IProps = Props & { [key: ?(not defined keys in Props)]: Node[] }
//...
constructor(props: IProps) {
// The API here is that any key not explicitly defined is
// an arbitrary node collection
// The following deconstruction is very common in JS, but
// TypeScript currently has no way to strongly type `children`, if the keys
// can be any string value
const { value, text, ...children } = props
// ...
} My workaround is essentially to do |
I'm hitting the same problem in JXA (JavaScript for Automation, in macOS), when a collection is of named objects. Consider, // This gets the Finder window with the name "Documents".
Application("Finder").windows.byName('Documents');
// This does the same thing.
Application("Finder").windows['Documents'];
// So does this, for the record, although it's not how the API is expected to be used.
Application("Finder").windows.Documents; In other words, |
I was playing around with branded strings for extra type safety. Then, I noticed this annoying thing, type MyKey = string & { someBrand: void };
type MyMap = Record<MyKey, number>;
declare const myMap: MyMap;
//const myKey: MyKey
declare const myKey: keyof MyMap;
/*
Expected: OK
Actual :
Element implicitly has an 'any' type because
expression of type 'MyKey'
can't be used to index type 'Record<MyKey, number>'
*/
console.log(myMap[myKey]); So, I made a workaround, function getWorkaround<K extends PropertyKey, V>(record : Record<K, V>, key : K) : V {
return record[key];
}
/*
Expected: OK
Actual : OK
*/
getWorkaround(myMap, myKey); 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 |
Seems like a legit requirement. Flow handles this just fine. |
Can we get some input from maintainers on this? It's sorely missed and difficult to find an open source solution without it. |
I cry every time I hit this arbitrary limitation. Also see #15746 which got closed. |
All the prior input we've provided is still true |
@RyanCavanaugh The last input you provided was to use mock Hungarian notation. People have provided a lot of substantive feedback that has been unaddressed. Since I'd just be repeating other people's points, see #1778 (comment) |
Branded strings are about the only reason I can see for this. And there's a workaround for branded strings as keys, even if not the best experience. |
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. |
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 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. |
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.
|
@matthew-dean But this issue is ONLY about allowing an alias instead of 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. |
I've returned to this issue because I've run into this problem again and again and it's driving me nuts.
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? |
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. type CustomerId = string;
type Customer = {
id: CustomerId;
name: string;
};
type CustomerRecord = { [CustomerId: string]: Customer }; |
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?" |
This +1000. Either types can be aliased or they can't. Either |
@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? |
@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: 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. |
Due to unresponsiveness on this issue, I'm raising another one: |
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. |
As a complete surprise, this just started working. type CellId = string;
type Row = {[cellId: CellId]: string}; // \o/
const row: Row = {'cell1': 'a'} Digging deeper, the new index signature features are in the 4.4 release notes. |
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! :) |
If you're using an older version of Typescript I have had success with a mapped object type instead. Arbitrary example, not tested, but should work with at least 4.2.4:
|
Generates the error:
It would be nice to be able to type indexes using string-aliases.
The text was updated successfully, but these errors were encountered: