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

'type' aliases of string can not be used as object index signature. #1778

Closed
jlennox opened this issue Jan 23, 2015 · 60 comments
Closed

'type' aliases of string can not be used as object index signature. #1778

jlennox opened this issue Jan 23, 2015 · 60 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@jlennox
Copy link

jlennox commented Jan 23, 2015

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.

@danquirk danquirk added the Suggestion An idea for TypeScript label Jan 23, 2015
@aelr
Copy link

aelr commented Feb 16, 2015

+1

@MicahZoltu
Copy link
Contributor

This includes the alias String.

class Map<T> {
    [key: String]: T
}

@MicahZoltu
Copy link
Contributor

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

An index expression argument must be of type 'string', 'number', 'symbol, or 'any'

And yes, there is a missing ' in the error message.

@RyanCavanaugh
Copy link
Member

String is not an alias for string. It refers to the non-primitive type String in JavaScript.

@MicahZoltu
Copy link
Contributor

Ah I didn't know that, thanks! I suppose this means I should be using string, number and boolean (lowercase) everywhere in my code rather than String, Number and Boolean. Most unfortunate.

@zpdDG4gta8XKpMCd
Copy link

what is the point in aliasing string?

@RyanCavanaugh RyanCavanaugh added Declined The issue was declined as something which matches the TypeScript vision and removed In Discussion Not yet reached consensus labels Jan 4, 2016
@RyanCavanaugh
Copy link
Member

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 a CustomerName.

There's already a place to write the intended index semantics (deviceId should probably be deviceGuid ?). 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.

@zpdDG4gta8XKpMCd
Copy link

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" [email protected] wrote:

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 a CustomerName.

There's already a place to write the intended index semantics (deviceId
should probably be deviceGuid ?). Adding a second place doesn't help
because it makes it look like the compiler will enforce the specific
indexing type when 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.


Reply to this email directly or view it on GitHub
#1778 (comment)
.

@jlennox
Copy link
Author

jlennox commented Jan 5, 2016

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.

That would indeed be more inline with what I'm after. Thank you for the update.

@Yona-Appletree
Copy link

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.

@niedzielski
Copy link

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();

@ehberger
Copy link

ehberger commented Jul 3, 2018

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.

 //nothing actually takes these runtime values, but they allow the type system to reason about categories of strings (including unions of id types)
type StudentId = 'any_student_id_value'
type ClassId = 'any_class_id_value'

to create them, you just need to cast a string (either directly, or wrapped in a constructor-like function:

const studentId = 'abc' as StudentId
const classId = 'abc' as ClassId

The type system behavior is quite sane for this use case, and you can do things like:

const studentToClass:{[id:StudentId]?:ClassId}

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).

@ondratra
Copy link

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?

@burtonator
Copy link

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!

@ducin
Copy link

ducin commented Jan 5, 2019

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 replace number with { amount: number, currency: Currency } in few hundred places, then good luck. Instead, you start with type Money = number, use Money 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 primitive number 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.

@ondratra
Copy link

ondratra commented Jan 6, 2019

@ducin That's an excellent point!

@zpdDG4gta8XKpMCd
Copy link

@ducin there is a better idea to that called units of measure, with a couple of implementations (near the bottom): #364

@ducin
Copy link

ducin commented Jan 6, 2019

@Aleksey-Bykov why "better" if DDD is a well-established and commonly used approach?

@zpdDG4gta8XKpMCd
Copy link

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

@jpike88
Copy link

jpike88 commented Oct 19, 2019

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:

  • strong arguments were made for implementing this feature across a range of use cases
  • a single weak justification was given for ignoring this feature, which was immediately rebutted
  • the justification has been downvoted heavily by users of Typescript
  • this feature has been closed anyway

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.

@matthew-dean
Copy link

matthew-dean commented Oct 19, 2019

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 [key: string]: any in order to not cause conflict with the existing keys, but of course that's a hack because I know the types; they're very explicitly defined in JS, but cannot be defined in TS, because the type provided by [key: string] has to intersect with the types of defined keys. The fact that ...rest objects can't be strongly typed in TypeScript is just an odd hole in the syntax (unless there's a workaround I haven't found yet?)

@Peeja
Copy link
Contributor

Peeja commented Dec 3, 2019

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, Application("Finder").windows is an object where every string key is a Window, except specifically for byName—and similarly byId and at—which are all functions. There appears to be no way to type this object in TypeScript.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Dec 29, 2019

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,

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);

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.

@jamesgpearce
Copy link

Seems like a legit requirement. Flow handles this just fine.

@noahbjohnson
Copy link

Can we get some input from maintainers on this? It's sorely missed and difficult to find an open source solution without it.

@AlexGalays
Copy link

I cry every time I hit this arbitrary limitation.

Also see #15746 which got closed.

@RyanCavanaugh
Copy link
Member

All the prior input we've provided is still true

@jlennox
Copy link
Author

jlennox commented Feb 5, 2020

@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)

@AnyhowStep
Copy link
Contributor

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.

@RyanCavanaugh
Copy link
Member

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:

This falls under the category of not appearing to do something you don't.

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.

@matthew-dean
Copy link

matthew-dean commented Feb 5, 2020

@RyanCavanaugh

It's actively confusing and doesn't create any new functionality.

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 to Foo must be a type of number (or a compatible type such as any).

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.

@aelr
Copy link

aelr commented Feb 6, 2020 via email

@dragomirtitian
Copy link
Contributor

@matthew-dean But this issue is ONLY about allowing an alias instead of string (or presumably number). 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.

@jpike88
Copy link

jpike88 commented Feb 14, 2020

I've returned to this issue because I've run into this problem again and again and it's driving me nuts.

@RyanCavanaugh

It's actively confusing and doesn't create any new functionality.

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.

public repeatableSections: { [s: string]: TemplateElement } = {};
public sectionHashMap: { [s: string]: TemplateElement } = {};
public fieldsThatHaveCrossElementChildren: { [s: string]: FormField } = {};
public elementHashMap: { [s: string]: FormElement } = {};

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.

public repeatableSections: { [ s: SomeInterface['someStringProperty'] ]: TemplateElement } = {};
public sectionHashMap: { [ s: SomeInterface['someStringProperty'] ]: TemplateElement } = {};
public fieldsThatHaveCrossElementChildren: { [ s: SomeInterface['someStringProperty'] ]: FormField } = {};
public elementHashMap: { [ s: SomeInterface['someStringProperty'] ]: FormElement } = {};

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?

@cyberixae
Copy link

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 };

@gravitypersists
Copy link

From the TS documentation:

Aliasing doesn’t actually create a new type - it creates a new name to refer to that type. Aliasing a primitive is not terribly useful, though it can be used as a form of documentation.

From the argument here:

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?

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?"

@matthew-dean
Copy link

matthew-dean commented Feb 21, 2020

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 '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, because string isn't a type the way types are types" is contrary to the rest of the language.

@jpike88
Copy link

jpike88 commented Feb 22, 2020

@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?

@jpike88
Copy link

jpike88 commented Feb 29, 2020

@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:

#1778 (comment)

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.

@jpike88
Copy link

jpike88 commented Mar 18, 2020

Due to unresponsiveness on this issue, I'm raising another one:

#37448

@phoenixeliot
Copy link

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.

@jamesgpearce
Copy link

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.

@sergii-zhuravel
Copy link

sergii-zhuravel commented Sep 21, 2021

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! :)

@acdoussan
Copy link

acdoussan commented Apr 7, 2023

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:

export type PersonName = string;
interface Person {
  name: string;
};

type NameToPerson = {
  [personName in PersonName]: Person;
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests