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";
}
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";
}
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.