Reputation: 129
I need some help on my TypeScript adventure.
Here's my example type:
type Target = {
names: string[]
addresses: {
location: string
}[]
};
Here's my example object:
const myTarget: Target = {
names: ["Alex", "Nico"],
addresses: [
{
location: "Miami",
},
{
location: "New York",
},
],
};
And here's my function example:
const changeValue = (
key: keyof Target,
value: string | Record<string, unknown>
) => {
myTarget[key] = [...myTarget[key], value]
}
changeValue('names', 'Elvis')
changeValue('addresses', { location: "Atlanta" })
I've an error on the myTarget[key]
saying this:
Cannot assign type '(string | Record<string, unknown>)[]' to type 'string[] & { location: string; }[]'.
Cannot assign type '(string | Record<string, unknown>)[]' to type 'string[]'.
Cannot assign type 'string | Record<string, unknown>' of type 'string'.
Cannot assign type 'Record<string, unknown>' to type 'string' .ts(2322)
From what I understand is that, for example, TypeScript is testing all combinations, including adding an object to the names
array, which is invalid, or adding a string inside addresses
array.
Can someone please help me to understand what's the problem and how can I handle it. ( the real project is so much more complicated, but maybe with this example I'll understand a bit..)
Thanks a lot.
Upvotes: 2
Views: 4033
Reputation: 33041
Why do you have an error ?
Answer on this question you can find here , here , here and in my article here.
Using this type string | Record<string, unknown>
for value
argument is not safe. It is better to infer the type of key
argument:
const changeValue = <Key extends keyof Target>(
key: Key,
value: Target[Key][number]
) => {
// error
myTarget[key] = [...myTarget[key], value]
}
You have an error here, because there is no correlation (inside) function body between key
and value
. Such correlation exists only outisde of function, I mean, when you call this function.
Please keep in mind that key
is still a union type.
I think it worth using immutable approach:
type Target = {
names: string[]
addresses: {
location: string
}[]
};
const myTarget: Target = {
names: ["Alex", "Nico"],
addresses: [
{
location: "Miami",
},
{
location: "New York",
},
],
};
const withTarget = <
Obj extends Record<string, unknown[]>
>(target: Obj) => <
Key extends keyof Obj
>(
key: Key,
value: Obj[Key][number]
): Obj => ({
...target,
[key]: target[key].concat(value)
})
const changeValue = withTarget(myTarget);
const result = changeValue('names', 'Elvis')
Upvotes: 0
Reputation: 44376
In general, where you want to pass a key of an object type and a value of the type of that key, you can do so like this:
const changeValue = <K extends keyof Target, V extends Target[K]>(
key: K,
value: V
) => {
myTarget[key] = value;
}
That is, you tell the compiler that value
is not only the same type of some value of the object, but the same type as the one given by key
.
However, you upped the difficult by playing with arrays. There is not, so far as I know, a utility type to extract the type of an array, so I wrote one:
type ArrayType<T> = T extends Array<infer U>? U : never;
Now we can use it:
const appendValue = <K extends keyof Target, V extends ArrayType<Target[K]>>(
key: K,
value: V
) => {
myTarget[key] = [...myTarget[key], value] as Target[K];
}
Unfortunately, as you see, the compiler gets a little confused about the relationship between V[]
and Target[K]
, so it needs the hint in the form of as
.
Now the calls compile cleanly:
appendValue('names', 'Elvis')
appendValue('addresses', { location: "Atlanta" })
Incidentally, you do some things in your example code (though perhaps not in your actual code) that I would urge you not to do.
First, you have mutable global data, myTarget
. Hic fons lacrimis, do not do this, in any language, if you can at all avoid it.
Also, mutating data in general is a bad idea. Consider this instead:
const appendValue = <K extends keyof Target, V extends ArrayType<Target[K]>>(
target: Target,
key: K,
value: V
): Target => ({
...target,
[key]: [...myTarget[key], value] as Target[K]
})
Upvotes: 5