How can I take the type { 'k': number, [s: string]: any }
and abstract over 'k'
and number
? I would like to have a type alias T
such that T<'k', number>
gives the said type.
Consider the following example:
function f(x: { 'k': number, [s: string]: any }) {} // ok
type T_no_params = { 'k': number, [s: string]: any }; // ok
type T_key_only<k extends string> = { [a in k]: number }; // ok
type T_value_only<V> = { 'k': V, [s: string]: any}; // ok
type T_key_and_index<k extends string, V> = { [a in k]: V, [s: string]: any };// ?
- Using
{ 'k': number, [s: string]: any}
directly as type of the parameter of the functionf
works. - Using the
[s: string]: any
indexed part intype
-alias works - Using
k extends string
intype
-alias also works - When combining the
k extends string
with the[s: string]: any
in sametype
-alias, I get a parse error (not even a semantic error, it doesn't even seem to be valid syntax).
This here seems to work:
type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V }
but here, I can't quite understand why it does not complain about extra properties (the type on the right side of the &
should not allow objects with extra properties).
EDIT:
It has been mentioned several times in the answers that the &
is the intersection operator, which is supposed to behave similarly to the set-theoretic intersection. This, however, is not so when it comes to the treatment of the extra properties, as the following example demonstrates:
function f(x: {a: number}){};
function g(y: {b: number}){};
function h(z: {a: number} & {b: number}){};
f({a: 42, b: 58}); // does not compile. {a: 42, b: 58} is not of type {a: number}
g({a: 42, b: 58}); // does not compile. {a: 42, b: 58} is not of type {b: number}
h({a: 42, b: 58}); // compiles!
In this example, it seems as if the {a: 42, b: 58}
is neither of type {a: number}
, nor of type {b: number}
, but it somehow ends up in the intersection {a: number} & {b: number}
. That's not how set-theoretical intersection works.
That's exactly the reason why my own &
-proposal looked so suspicious to me. I'd appreciate if someone could elaborate how "intersecting" a mapped type with { [s: string]: any }
could make the type "bigger" instead of making it smaller.
I've seen the questions
but those did not seem directly related, albeit having a similar name.
type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V }
is the correct way to define the type you are after. But one thing to know is (paraphrasing deprecated flag: keyofStringsOnly):I do not know a method to restrict index to be just the
string
type and notstring | number
. Actually allowingnumber
to accessstring
index seems a reasonable thing, as it is in line how Javascript works (one can always stringify a number). On the other hand you cannot safely access a number index with a string value.The
&
type operator works similarly to set theoretic intersection - it always restricts set of possible values (or leaves them unchanged, but never extends). In your case the type excludes any non-string-like keys as index. To be precise you excludeunique symbol
as index.I think your confusion may come from the way how Typescript treats function parameters. Calling a function with explicitly defined parameters behaves differently from passing parameters as variables. In both cases Typescript makes sure all parameters are of correct structure/shape, but in the latter case it additionally does not allow extra props.
Code illustrating the concepts:
EDIT
Object literals - explicitly creating object of some shape like
const p: { a: number} = { a: 42 }
is treated by Typescript in a special way. In opposite to regular structural inference, the type must be matched exactly. And to be honest it makes sense, as those extra properties - without additional possibly unsafe cast - are inaccessible anyway.TS Handbook
The other option to get around this error is to... intersect it with
{ [prop: string]: any }
.More code: