Search code examples
typescript

Strictly flavored key type in mapped type


I'm using Flavored types to catch errors where a user name is used in place of a user id. However, I'm having trouble when the flavored type is a key of a dictionary. I'd like to make a dictionary which "strictly" contains keys of a certain flavor. However, if I use the wrong key type, TypeScript keeps inferring 'any' instead of 'never' or 'undefined'.

declare const flavor: unique symbol;
type Flavored<T, F> = T & { readonly [flavor]?: F };

type UserId = Flavored<string, 'UserId'>;
type UserName = Flavored<string, 'Name'>;
type Color = Flavored<string, 'Color'>;

const alice: UserName = 'alice';
const id: UserId = 'user-0001';
const blue: Color = 'blue';
const yellow: Color = 'yellow';

type ColorsByUserId = {
    [Key in UserId]: Color[];
} & {
    [Key in Exclude<PropertyKey, UserId>]: undefined;
}

// Unexpected error:
// Type '{ [x: string]: Color[]; }' is not assignable to type 'ColorsByUserId'.
//   Type '{ [x: string]: Color[]; }' is not assignable to type '{ [x: number]: undefined; [x: symbol]: undefined; }'.
//     'string' and 'number' index signatures are incompatible.
//       Type 'Color[]' is not assignable to type 'undefined'.
const favorites: ColorsByUserId = {
    [id]: [ blue, yellow ],
};

// Should have error here because alice is not a UserId. Instead we get color as 'any'.
for (const color of favorites[alice]) {
    console.log(color);
}

Solution

  • Remember to check if you have Strict Mode or noImplicitAny on: https://www.typescriptlang.org/tsconfig/#noImplicitAny

    If you are using VS Code's built-in TypeScript without a tsconfig file, then Strict Mode is off by default.

    This turned out to be my original problem.