最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

typescript - Strictly flavored key type in mapped type - Stack Overflow

programmeradmin3浏览0评论

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
  • 1 why not to use just 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
  • @AlexanderNenashev, I just tested on TypeScript 5.7.3, and this does not work. The "color" variable is detected as "any" type, rather than "undefined". – Nycki Commented Feb 15 at 1:54
  • @AlexanderNenashev UPDATE: my mistake, I had UserId and UserName swapped in my test environment. You are correct. Submit this as an answer and I'll accept it! – Nycki Commented Feb 15 at 2:01
Add a comment  | 

3 Answers 3

Reset to default 1

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

发布评论

评论列表(0)

  1. 暂无评论