How to create union of original type and custom type with some properties of original changed and rest the same?

Viewed 45

I am trying to write typings for a function that is supposed to take an object and some of its keys as arguments and returns a new object where the value of specified keys can be the original or a Set/Array of original value type.

For example : If we were to pass an object of the following type :

interface Example {
    a: number
    b: number
    c: string
}

and pass a and c as the keys, then the returned type would be

{
    a: number | Set<number> | number[]
    b: number
    c: string | Set<string> | string[]
}

I have manages to do this by defining the following types

type SetOrArray<T, K extends (keyof T)[]> = { [V in K[number]]: Set<T[V]> | T[V][] }
type UnionType<T, K extends (keyof T)[]> = T | SetOrArray<T, K>

But when I try this with an object, then only the keys passed (in K) are preserved in the resulting UnionType and the remaining keys, which should have just been present with their original types due to T | union, are not existing on the UnionType.

For example:

function afunc<T, K extends (keyof T)[])>(obj: T, ...keys: K): UnionType<T, K> {
    return obj // just for example
}

const obj = {a: 1, b: 2, c: "apple"}
const res = afunc(obj, "a", "c")
// res.a exists with type number | Set<number> | number[]
// res.c exists with type string | Set<string> | string[]
// but res.b does not exist, whereas it should have had type number
2 Answers

I just added one additional type for missing field, Try this code:

interface Example {
    a: number
    b: number
    c: string
}

type SetOrArray<T, K extends (keyof T)[]> = { [V in K[number]]: T[V] | Set<T[V]> | T[V][] }
//Use exclude to create object with missing field  
type Outersect<T, K extends (keyof T)[]> = { [P in Exclude<keyof T, keyof SetOrArray<T, K>>]: T[P] }
//And union with SetOrArray type
type UnionType<T, K extends (keyof T)[]> = Outersect<T, K> & SetOrArray<T, K>

function afunc<T, K extends (keyof T)[]>(obj: T, ...keys: K): UnionType<T, K> {
    return obj // just for example
}


const obj = { a: 1, b: 2, c: "apple" }
const res = afunc(obj, "a", "c")

const test1 = res.a;
const test2 = res.b;
const test3 = res.c;

I found an answer to this, which is pretty much what Aleksandre Javakhishvili did below, just works better and is more concise.

It is done by using Conditional Types


// If the V is also present in K, then create union type else the original value type 
type UnionType<T, K extends keyof T> = {
    [V in keyof T]: (V extends K ? Set<T[V]> | T[V][] | T[V] : T[V])
}

function afunc<T, K extends (keyof T)[]>(obj: T, ...keys: K): UnionType<T, K[number]> {
    return obj // just for example
}

const obj = { a: 1, b: 2, c: "apple" }
const res = afunc(obj, "a", "c")

const test1 = res.a;
const test2 = res.b;
const test3 = res.c;
Related