Home > Software engineering >  Typescript: Implement Union Type
Typescript: Implement Union Type

Time:09-15

I know that implementing Union Types is not possible in TS, what is absolutely reasonable. But I'm probably looking for something similar / else:

type TypeA = { a: string, b?: string }
type TypeB = { a: string, c?: string }
type UnionType = TypeA | TypeB;
type IntersectionType = TypeA & TypeB;

// Error:
// A class can only implement an object type or intersection of object types with statically known members.(2422)
class UnionClass implements UnionType {
    checkUnionProperties() {
        let x: UnionType = { a: '' };
    }

    a = 'a';
}

//This is possible
class IntersectClass implements IntersectionType {
    checkUnionProperties() {
        let x: IntersectionType = { a: '' };
    }

    a = 'a';
}

I'd like to be able to implement a Type / Interface, that has all the properties the Types have in common (in this case only a: string), but not really a class that is of TypeA or TypeB. Is there a language-feature I'm looking for?

CodePudding user response:

In this comment, jcalz points to the fact that Pick<T, keyof T> when T is a union type provides a type which only has the common parts:

type TypeA = { a: string; b?: string; };
type TypeB = { a: string; c?: string; };
type UnionType = TypeA | TypeB;

type Common<T> = Pick<T, keyof T>;

type X = Common<UnionType>;
//   ^? − type X = { a: string; }

Playground link. (For the avoidance of doubt, it's true even when the other properties — b and c in your example — aren't optional.)

You can implement the result:

type TypeA = { a: string; b?: string; };
type TypeB = { a: string; c?: string; };
type UnionType = TypeA | TypeB;

type Common<T> = Pick<T, keyof T>;

class UnionClass implements Common<UnionType> {
    checkUnionProperties() {
        let x: UnionType = { a: '' };
        console.log(x);
    }

    a = "a";
}

Playground link

Here's an example without a = "a"; in the class, so the class doesn't implement the interface correctly — and it is indeed an error as you'd hope.


User makeitmorehuman points to this question in a comment, which has this excellent answer from qiu that handles it differently if TypeA and TypeB both have a property with the same name but different types. For instance, if you had x: string in TypeA but x: number in TypeB, the Common<TypeA | TypeB> above would result in a type with x: string | number. That may be what you want, but if not, qiu's answer has you covered with the SharedProperties type (see the answer for details), which would leave x out entirely:

// `OmitNever` and `SharedProperties` from: https://stackoverflow.com/a/68416189/157247
type OmitNever<T extends Record<string, unknown>> = {
    [K in keyof T as T[K] extends never ? never : K]: T[K];
};
type SharedProperties<A, B> = OmitNever<Pick<A & B, keyof A & keyof B>>;

Your class could use that, like this:

class UnionClass implements SharedProperties<TypeA, TypeB> {
    checkUnionProperties() {
        let x: UnionType = { a: "" };
        console.log(x);
    }

    a = "a";
}

Playground link

It doesn't make a difference for the TypeA and TypeB shown, since they don't have properties with the same names but different types (like x in my description above), but it would if they did — Common<TypeA | TypeB> would include x as string | number, but SharedProperties<TypeA, TypeB> leaves it out entirely.

  • Related