Reputation: 3436
Consider the following mapped type definition:
type FeaturesMapEx<T> = {
[K in keyof T]?: T[K];
};
Which is a valid, generic mapped type, that uses generic type T
and T
's fields (keys) as an own index type. Those fields hold value of respective T[K]
type, which basically translates to
"Here you have a generic FeaturesMapEx type of T
. Use T
to obtain it's fields and use them as your own. If T[K]
is type X
, then FeaturesMapEx<T>[K]
is same type"
Ok. Now the thing I don't really get, why the declaration does not need to be like the following, using two generic parameters, one to represent Type, second to represent Keys of those Type:
export type FeaturesMapEx<T, K extends keyof T> = {
[K in keyof T]?: T[K];
};
The above declaration is valid, but K is unused, [K in keyof T]
uses a different K
, than the one defined in generic declaration.
Or even better (but not valid) declaration that makes more common sense for me:
export type FeaturesMapEx<T, K extends keyof T> = {
[K]?: T[K];
};
Which gives TypeScript error as following:
A computed property name in a type literal must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1170)
'K'only refers to a type, but is being used as a value here.ts(2693)
While feeling comfortable with the usage of mapped types and generic this example always makes me a bit dizzy when I've got to write it on my own after some break, I'm always falling into later presented patterns (two generics or "type used as value" error)... can anyone explain is my confusion common or I'm missing something?
A bonus question is, from where does the type K
comes into the first declaration, If it's not provided by the generic type parameter?
Upvotes: 3
Views: 4548
Reputation: 19957
Technically speaking, there's no "why" here, it's just the way TS syntax work, we just have to learn it.
The expression [K in keyof T]
involves two type operators.
keyof T
, the index type query operator, which is quite easy to understand, just infer the keys of an interface.{ [/* one type param */ in /* some unique symbol union type value */]: /* some type value */}
Yes you see me right, this whole thing, including pair of {}
and []
, one column :
, one in
keyword, can be viewed as a type operator applicable to two type value operands. As of K
is a param here, not a value.
(actually I'm not sure if you can call such thing operator anymore? more like a flow control structure, either way, it's a fixed thing that has rules in its syntax and usage)
Again it's a whole thing. You cannot add other member to it, that gives you syntax error.
type foo<T> = {
[K in keyof T]: boolean;
other: boolean; // <-- syntax error
}
But we as human has a need to conceptually understand the "why"! I have an analogue for you. Think of generic type params as "degrees of freedom", or independent variables.
If one piece of information exists independent of others, it then qualifies as a generic param. So in your example:
type foo<T, K extends keyof T> = {
[K in keyof T]?: T[K];
};
K
does not qualify as a param, cus all about K
can be inferred from T
, it has 0 degrees of freedom.
Upvotes: 1
Reputation: 141512
...from where does the type K comes into first declaration, if it's not provided by generic type parameter?
In your first declaration, the type K
is introduced in the loop.
type FeaturesMapEx<T> = {
[K in keyof T]?: T[K]; // this is the loop
};
Here is a loose analogy that uses a JavaScript function:
const someObj = {
foo: 'foo-value',
bar: 'bar-value',
baz: 'baz-value',
}
function featuresMapEx(T) {
return Object.keys(T).map(K => `${K}: ${T[K]}`);
}
const result = featuresMapEx(someObj);
console.log(result);
Upvotes: 1
Reputation: 328132
Consider the interface
interface Foo {
a: string,
b: number
}
and the mapped type
export type FeaturesMapEx<T> = {
[K in keyof T]?: T[K];
};
In such a mapped type we are introducing a new type parameter K
(that's where it comes from) which is iterating over keyof T
. Assuming that keyof T
is a union type, K
iterates successively over each constituent of that union. You can kind of think of it as the type-level version of the following run-time for
loop:
function featuresMapEx(t: any) {
for (let k of Object.keys(t)) {
t[k];
}
}
Note how t
is introduced in the function signature (like the T
in the type alias definition) while k
is introduced in a loop only exists in the scope of that loop (like the K
is introduced in the mapped type key, and only exists in the scope of the mapped type property).
So FeaturesMapEx<Foo>
becomes {a: string, b: number}
.
If I wrote the following JavaScript function instead:
function featuresMapEx(t: any, k: any) { // unused parameter here
for (let k of Object.keys(t)) { // oops
t[k];
}
}
you can see that you are completely ignoring the k
passed into the function, much like
export type FeaturesMapEx<T, K extends keyof T> = { // unused parameter here
[K in keyof T]?: T[K]; // oops
};
ignores the K
passed into FeaturesMapEx<T, K>
.
So this FeaturesMapEx<Foo, 'a'>
still becomes {a: string, b: number}
.
As for your other syntax:
export type FeaturesMapEx<T, K extends keyof T> = {
[K]?: T[K]; // error
};
That would no longer be a mapped type at all, but an index signature, and you currently can't use arbitrary key types in an index signature (although this might happen after TS3.5 sometime). A mapped type looks like an index signature but it isn't one. Even if that ends up being part of the language, it wouldn't really be the same thing... it would probably act like this:
function featuresMapEx(t: any, k: any) {
t[k]; // just one thing
}
That is, it wouldn't really iterate over anything. You can kind of get this behavior now like this:
export type FeaturesMapEx<T, K extends keyof T> = Partial<Record<T, K>>
which is the same as the mapped type
export type FeaturesMapEx<T, K extends keyof T> = {
[P in K]?: T[K]
}
but this FeaturesMapEx<Foo, keyof Foo>
becomes {a: string | number, b: string | number}
. That is, the keys would be right, but their values would be the union of the values you expect.
Anyway, I hope that the comparison to runtime functions helps it "click" for you. Good luck!
Upvotes: 4