I have several types that are made up of a large number of intersectioned types. I would like to extract these types into what they eventually compile into. I believe this will be helpful when it comes choosing the way to refactor.
An example. Given this setup:
type BigType = {
thing: string
}
type LargeType = {
blah: boolean
}
type HugeType = BigType & LargeType
I would like some way of outputting:
type HugeType = {
thing: string
blah: boolean
}
This is a small example - in reality there are many more types intersected together. Ideally I would output the leaves of these type "trees" - the biggest resulting types.
Some more information about why. We have some copy-pasted code, with types, views (React components) and business logic (RxJS streams) duplicated. Speed was necessary, so entire folders were copy-pasted and the code tweaked for a new set of functional requirements. We would now like to reduce this duplication.
We have view models encoded as Typescript types in between the RxJS streams and React component trees. With the goal of reducing duplicate code, one approach we are discussing is pulling these view models into a class hierarchy and sharing common datapoints in superclasses, altering types in business logic and view side. After these replacement models are in place, the next step would be refactoring both sides to reduce the common code operating on the same types.
So I would like to construct these types (view models) from the intersecting types because I think it will be beneficial when working out how much is shared between these different yet very similar view models. Feedback welcome on this approach.
CodePudding user response:
If the types you are intersecting have only named members (so they do not also act as function types or constructor types) then you can get the behavior you're looking for via a mapped type, which iterates over all named members of a type (even an intersection type) and produces a property for each member, based on a rule. If you make the mapped type an identity map where each property is mapped to itself:
type Id<T> = { [K in keyof T]: T[K] };
then you can turn an intersection type into an equivalent single-object type:
type HugeType = Id<BigType & LargeType>;
/* type HugeType = {
thing: string;
blah: boolean;
} */
That worked out just fine, but sometimes the compiler likes to preserve type alias names like Id
in IntelliSense quick info. If you hover over HugeType
and it does show you Id<BigType & LargeType>
, that would be worse than just BigType & LargeType
.
If you run into that problem, one way I know of to tell the compiler not to preserve such names is to use conditional type inference to "copy" a type into a new type parameter, and then use the type parameter in place of the original type:
type HugeType = (BigType & LargeType) extends infer H ?
{ [K in keyof H]: H[K] } : never;
/* type HugeType = {
thing: string;
blah: boolean;
} */
It's doing the same thing as before; we are performing an identity mapping over BigType & LargeType
, but now there isn't any type alias named Id
involved, so it can't be present in IntelliSense.
Edit: Oh, I see you're talking about "trees". If by this you mean that the object properties themselves have properties which you might need to merge this way, then you can use a recursively-defined identity mapped type like this (and here the conditional type inference really does help):
type IdRecursive<T> = { [K in keyof T]: IdRecursive<T[K]> } extends
infer I ? { [K in keyof I]: I[K] } : never
And test it:
type X = {
a: {
b: {
c: number
}
}
}
type Y = {
a: {
b: {
d: string
}
}
}
type XY = IdRecursive<X & Y>
/* type XY = {
a: {
b: {
c: number;
d: string;
};
};
} */