Reputation: 143
Why S
is string
in following code?
type T<X> = {[K in keyof X]: never};
type S = T<string>;
Here is TypeScript playground. https://www.typescriptlang.org/play?#code/C4TwDgpgBAKgPADQHxQLxQN4G0DSUCWAdlANYQgD2AZlAgLoBcUhEAbhAE4C+A3AFChIUAMppYcAM7AORAOZIeQA
It becomes the same type (string
) even if I replace never
by other types.
Adding context, following type works as I expect.
type T<X> = {[K in (keyof X) & string]: never};
type S = T<string>;
Upvotes: 2
Views: 820
Reputation: 13243
Below is a workaround for mapping primitive types:
// doesn't work for primitive types
type MappedTypeNoPrimitive<T> = {
[K in keyof T]: never /* any mapping */
}
// also works for primitive types
// Note the similarity to `Pick<T, keyof T>`
type MappedType<T, Keys extends keyof T> = {
[K in Keys]: never /* any mapping */
}
type MappedString = MappedType<string, keyof string>
/*
type MappedString = {
readonly [x: number]: never;
[Symbol.iterator]: never;
toString: never;
charAt: never;
charCodeAt: never;
concat: never;
indexOf: never;
lastIndexOf: never;
localeCompare: never;
match: never;
replace: never;
... 39 more ...;
at: never;
}
*/
Explanation: It seems that the [K in keyof T]
part in MappedTypeNoPrimitive
recognizes primitive T
types, e.g., [K in keyof string]
and returns string
, ignoring the mapping. Using Keys
in MappedType
seems to avoid this.
KeyOf<T>
If you want to map primitive types in multiple places you can use
// converts any type (objects, primitives, functions) to an object `{...}`
// Notes: I don't know why, but we need `T extends object` such that `KeyOf<T>` works
type ToObject<T> = T extends object ? T : Pick<T, keyof T>
// returns `keyof T` such that primitive types can be mapped
// Notes: We need `& keyof T` such that typescript can use `KeyOf<T>` in a mapped type
type KeyOf<T> = keyof ToObject<T> & keyof T
// Works also with primitive types
type MappedType2<T> = {
[K in KeyOf<T>]: never /* any mapping */
}
Upvotes: 0
Reputation: 9310
Essentially what you are doing is mapping the keys of the type string
which are number | typeof Symbol.iterator | "toString" | "charAt" | "charCodeAt" | "concat" | "indexOf" | "lastIndexOf" ...
etc. set their types to never
. You can do that with a simple Union.
type X = keyof string; // type X = number | typeof Symbol.iterator | "toString" ...
But not with key mapping the way you tried. What you created is a so called homomorphic mapped type. That means the compiler regognizes you are trying to map the keys of an already existing object type. When that is the case the compiler simple returns a type with the exact same property modifiers as the one on your input type.
In short, when mapping the properties of a primitive type typescript will just return that very primitive type.
This mapped type returns a primitive type, not an object type. Mapped types declared as
{ [ K in keyof T ]: U }
whereT
is a type parameter are known as homomorphic mapped types, which means that the mapped type is a structure preserving function ofT
. When type parameterT
is instantiated with a primitive type the mapped type evaluates to the same primitive. - TypeScript FAQ Bugs that aren't Bugs
Upvotes: 3