Touk
Touk

Reputation: 129

TypeScript: Typing a value based on an object key

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

Answers (2)

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

Michael Lorton
Michael Lorton

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

Related Questions