Chet
Chet

Reputation: 19839

How to find and disambiguate a union type from a list of union types in TypeScript?

I have a basic union type;

    type A = {type: "A"}
    type B = {type: "B"}
    type X =  A | B

And I have a function that finds an item in a list that has the same type:

    function find(x: X, list: Array<X>) {
        return list.find(item => item.type === x.type)
    }

I would expect the return type of this function to be the specific subtype of X that matches the input x. That is, I want find({type: "A"}, [{type: "A"}, {type: "B"}]) to return a type of A.

Any ideas how I might do this?


Edit: It turns out what I'm dealing with is a little more complicated. I have a concept of batches that are queue and I want to add an item to a batch if it exists, otherwise I want to enqueue a new batch:

    type A = { type: "A" }
    type B = { type: "B" }
    type X = A | B

    type Batch<T extends X> = { type: T["type"]; batch: Array<T> }
    type BatchA = Batch<A>
    type BatchB = Batch<B>
    type BatchTypes = BatchA | BatchB

    function find(x: X, list: Array<BatchTypes>) {
        return list.find(item => item.type === x.type)
    }

    function enqueue(x: X, queue: Array<BatchTypes>) {
        const result = find(x, queue)
        if (result) {
            result.batch.push(x)
        } else {
            queue.push({type: x.type, batch:[x]})
        }
    }

    let a: A
    let b: B
    let queue: Array<BatchTypes>

    enqueue(a, queue)
    enqueue(b, queue)

The problem here lies in enqueue because result a union of both types. When I try to overload the types, result is resolved properly but there's an issue with the first argument of find and pushing a new batch to the queue:

    function find(x: A, list: Array<BatchTypes>): BatchA
    function find(x: B, list: Array<BatchTypes>): BatchB
    function find(x: X, list: Array<BatchTypes>) {
        return list.find(item => item.type === x.type)
    }

    function enqueue(x: A, queue: Array<BatchTypes>)
    function enqueue(x: B, queue: Array<BatchTypes>)
    function enqueue(x: X, queue: Array<BatchTypes>) {
        const result = find(x, queue)
        if (result) {
            result.batch.push(x)
        } else {
            queue.push({ type: x.type, batch: [x] })
        }
    }

Please let me know if there's a better way of clarifying this question.


Given @artem's answer, I've gotten closer:

    function find<T extends X>(x: T, list: Array<BatchTypes>): Batch<T> {
        return <Batch<T>>list.find(item => item.type === x.type)
    }

    function enqueue<T extends X>(x: T, queue: Array<BatchTypes>) {
        const result = find(x, queue)
        if (result) {
            result.batch.push(x)
        } else {
            queue.push({ type: x.type, batch: [x] })
        }
    }

But there's still an issue with queue.push.


Maybe this is a more concise example of demonstrating the current issue:

    type A = { type: "A" }
    type B = { type: "B" }
    type X = A | B

    let list: Array<Array<A> | Array<B>>

    function append<T extends X>(x: T) {
        list.push([x])
    }

    function append2(x: X) {
        list.push([x])
    }

Upvotes: 2

Views: 1228

Answers (1)

artem
artem

Reputation: 51629

You can do that by adding overload declarations for find:

type A = {type: "A"}
type B = {type: "B"}
type X = A | B

function find(a: A, list: Array<X>): A;
function find(a: B, list: Array<X>): B;
function find(x: X, list: Array<X>) {
    return list.find(item => item.type === x.type)
}

let a: A;
let b: B;
let x = [a, b];

let a1 = find(a, x); // inferred as A
let b1 = find(b, x); // inferred as B

If find return type will always be the same as the type of its first argument, you can use single generic overload declaration to avoid repeating:

function find<T extends X>(x: T, list: Array<X>): T;
function find(x: X, list: Array<X>) {
    return list.find(item => item.type === x.type)
}

Upvotes: 1

Related Questions