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);
}
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);
}
Share
Improve this question
edited Feb 14 at 23:23
jonrsharpe
122k30 gold badges267 silver badges474 bronze badges
asked Feb 14 at 23:18
NyckiNycki
96010 silver badges19 bronze badges
3
|
3 Answers
Reset to default 1Seems the & {[Key in Exclude<PropertyKey, UserId>]: undefined;}
is redundant, TS will check anyway if the index isn't UserId
. Remove that and you get a proper error where you try to index by UserName
:
Playground
type ColorsByUserId = {
[Key in UserId]: Color[];
}
const favorites: ColorsByUserId = { // ok
[id]: [ blue, yellow ],
};
// Element implicitly has an 'any' type because expression of type 'UserName' can't be used to index type 'ColorsByUserId'.
for (const color of favorites[alice]) {
console.log(color);
}
Exclude<PropertyKey, UserId>
is not doing what you think it's doing, past me! The excluded type resolves to never
, which is equivalent to not defining a key at all. What you have is equivalent to this:
type ColorsByUserId = {
[x: string & { readonly [flavor]?: 'UserId' }]: Color[];
} & {
[x: number]: undefined;
[x: symbol]: undefined;
}
None of these mappings allow 'plain strings' as keys, so TypeScript is trying to treat your UserId key as if it were a number, I think? This feels like a bug in TypeScript.
As a workaround, give TypeScript a 'fallback type' which is compatible with your specific type, but specify that the fallback type may be undefined. Now TypeScript knows that the result might be a Color[]
or it might be undefined
, but it definitely won't be any
, so you get the error you want.
type ColorsByUserId = {
[Key in PropertyKey]: Color[] | undefined;
} & {
[Key in UserId]: Color[];
}
const favorites: ColorsByUserId = {
[id]: [ blue, yellow ],
};
// TypeError: Object is possibly 'undefined'.
for (const color of favorites[alice]) {
console.log(color);
}
Remember to check if you have Strict Mode or noImplicitAny on: https://www.typescriptlang./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.
type ColorsByUserId = { [Key in UserId]: Color[]; }
. works as expected, for example gives a proper error in the second case – Alexander Nenashev Commented Feb 14 at 23:28