TLF
TLF

Reputation: 85

Indexing into Typescript Record with opaque type

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

Answers (2)

avdotion
avdotion

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

Evert
Evert

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

Related Questions