Reputation: 85
The following code is giving me a TypeScript error:
type UserID = string & { readonly _: unique symbol };
interface Chat {
name: string
}
type AllChats = Record<UserID, Chat>;
const test: AllChats = {}
const userID = "ads" as UserID;
test[userID] = {
name: "my chat"
}
I'm using UserID as an opaque type (like described here https://evertpot.com/opaque-ts-types/), so it acts a string, however random strings cannot be assigned to a variable of type UserID.
However, I'm getting the following error on the last line: "Element implicitly has an 'any' type because expression of type 'UserID' can't be used to index type 'Record<UserID, Chat>'."
Any idea why I'm getting this error? I don't see why I can't use something of type UserID into index into a record of type Record<UserID, Chat>.
UPDATE: According to this page https://levelup.gitconnected.com/building-type-safe-dictionaries-in-typescript-a072d750cbdf, it looks like I can achieve the behavior I want by using Javascript Maps instead of objects. But this means giving up on all the object syntax that Javascript offers, which I'd rather not do.
UPDATE2: Looks like this might be a known issue https://github.com/microsoft/TypeScript/issues/15746 that Typescript doesn't handle right now
Upvotes: 2
Views: 1099
Reputation: 137
Update 29 Jan 2022, TypeScript 4.5.4: it works.
type OPAQUE_MODIFIER = '_';
export type Opaque<T extends string> = `${OPAQUE_MODIFIER}${T}${OPAQUE_MODIFIER}${string}`
type Apple = Opaque<'apple'>;
type Orange = Opaque<'orange'>;
// Test 1
const apple: Apple = '1' as Apple;
// @ts-expect-error
const orange: Orange = apple;
// Test 2
const appleCollection: Record<Apple, string> = {
[apple]: 'value',
// @ts-expect-error <----------- The only one case that fails :(
[orange]: 'value',
};
// Test 3
appleCollection[apple] = 'newValue';
// @ts-expect-error
appleCollection[orange] = 'newValue';
The solution may be template literals as indexes. But this feature is not supported yet. However, it is in current milestone (TS 4.3.(0,1)). Check this out: https://github.com/microsoft/TypeScript/issues/42192.
type OPAQUE_MODIFIER = '__OPAQUE__';
type EntityId = `${OPAQUE_MODIFIER}user${OPAQUE_MODIFIER}${string}`;
type Entity = {
title: string,
};
type EntityCollection = Record<EntityId, Entity>;
const collection: EntityCollection = {
// should works as expected (1)
['entity 1 id' as EntityId]: {
title: 'entity 1',
},
// should throws an error (2)
['entity 2 id' as string]: {
title: 'entity 2',
},
};
// should works as expected (3)
collection['entity 3 id' as EntityId] = {
title: 'entity 3',
}
// should throws an error (4)
collection['entity 4 id' as string] = {
title: 'entity 4',
}
Related issues:
Upvotes: 0
Reputation: 99525
Indeed this is not possible. This is due to the fact that objects can only have numeric, string or symbol keys and not other, non-primitive types.
Even if it compiles down to something that would work, typescript in this case can't know this due to the earlier unique symbol
trick.
Using a Map
is probably indeed the easiest way to do this. Maps have a pretty nice API that might not find annoying.
Upvotes: 2