4

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?

2

1 Answer 1

6

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Super great and detailed answer! Thank you so much!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.