Skip to content

Inconsistent behaviour of never within a mapped type #61602

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Closed
1 task done
juhort opened this issue Apr 21, 2025 · 2 comments
Closed
1 task done

Inconsistent behaviour of never within a mapped type #61602

juhort opened this issue Apr 21, 2025 · 2 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@juhort
Copy link

juhort commented Apr 21, 2025

Acknowledgement

  • I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion.

Comment

Why are the results different in the following cases?

type T = { [K in keyof never]: string };
//   ^? type T = {}
type Keys = keyof never;
//   ^? type T = string | number | symbol

type T = { [K in Keys]: string };
//   ^? type T = {
//          [x: string]: string;
//          [x: number]: string;
//          [x: symbol]: string;
//      }

The second example makes sense to me — keyof never evaluates to string | number | symbol, so we get back index signatures as expected. But it's unclear why the first example produces an empty object instead.


Also, when generics are involved the result becomes never. This should also have been an index signature.

type Test<T> = { [K in keyof T]: string };

type T = Test<never>;
//   ^? type T = never
@RyanCavanaugh
Copy link
Member

FAQ bot response.


Your observations follow naturally from how TypeScript treats the special type "never" in mapped types. "never" is effectively the empty union, so when it's used in certain contexts like a mapped type, you end up with no members to iterate over, resulting in an empty object or the type "never". Conversely, when you write:

type Keys = keyof never;

then "keyof never" can appear to be "string | number | symbol". In that case, TypeScript is using the fallback for the property keys that an object type could have. But in a mapped type, the compiler does not see any actual members to operate on (because "never" is an empty union), so the result is an empty object:

type T = { [K in keyof never]: string };
//  ^? type T = {}

Regarding generics, the same principle applies. If you do:

type Test<T> = { [K in keyof T]: string };

type T = Test<never>;
//  ^? type T = never

there is nothing to map over when you pass never, so TypeScript ends up with "never" instead of building a meaningful type out of non-existent keys. This is related to how never is “the empty union” and can distribute to never in certain contexts. Specifically, if a conditional type tests each member of a union, there is nothing to test when you pass never.

When conditional types act on a generic type, they become distributive when that type could be a union. If your function or type uses an extends any-style check, TypeScript evaluates each member of the union separately — including the empty union never. You see that explicitly in examples like:

type IsString<T> = T extends string ? "yes" : "no";

type S = IsString<string>;
// "yes"

type SB = IsString<string | boolean>;
// "yes" | "no"

type N = IsString<never>
// never

The same thinking extends to mapped types and explains why you get an empty object in one scenario ([K in keyof never]) and the fallback index signatures ([K in string | number | symbol]) when referring directly to keyof never. If you want to avoid this kind of distributive behavior with conditional types, you can wrap each side of the extends clause with square brackets, like:

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;

// This type is no longer a union of arrays
// but rather (string | number)[]
type ArrayOfStrOrNum = ToArrayNonDist<string | number>;

Additional context: The second behavior follows from homomorphic mapped types being distributive. I don't recall the reason for { [K in keyof never]: string } being {} but there's really no reason to write that in a nongeneric position

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Apr 21, 2025
@juhort
Copy link
Author

juhort commented Apr 22, 2025

TIL that homomorphic mapped types behave like distributive conditionals when instantiated with never. A non-homomorphic mapped type would return the expected index signatures object.

type Test<T> = { [K in Exclude<keyof T, never>]: string };

type T1 = Test<never>;
//   ^? type T1 = {
//          [x: string]: string;
//          [x: number]: string;
//          [x: symbol]: string;
//      }

I don't recall the reason for { [K in keyof never]: string } being {} but there's really no reason to write that in a nongeneric position

Yeah true, I was mostly concerned with the homomorphic case, but discovered this case while playing around.

@juhort juhort closed this as completed Apr 22, 2025
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

2 participants