How to constrain argument to supertype but keep reference to full type for return value?
I'm trying to do a stunt in deduplicating code with full type safety. Basically, in my project I have a bunch of things that all should extend the same interface but they don't because the property names vary in each type. I cannot just change them, because they are tied to the database. Which I know is generally bad but in this particular case it's awesome.
So, I want to write a function that can operate on all of these different types by passing in extra info that acts as an adapter. I'm able to make it work, but I loose the type information of the object I'm working on. I need to keep it, because the return value should have that type, not some supertype.
Here's the code:
interface Sized {
width: number;
height: number;
}
interface Positioned {
x: number;
y: number;
}
/**
* Adjusts `element`'s position so that it fits into the given `container`.
*
* `element` and `container` can have arbitrary property names for their dimensions.
* `elKeys` and `cKeys` map them to standard `x`, `y`, `width` and `height` properties.
*
* `elKeys` and `cKeys` must be declared using "as const" assertion,
* so Typescript is aware of their literal types to be able to check
* that they are compatible.
*/
function fitElementIntoContainer<
E extends string,
C extends string
>(
element: { [key in E]: number },
container: { [key in C]: number },
elKeys: string extends E ? never : { [key in keyof (Sized & Positioned)]: E },
cKeys: string extends C ? never : { [key in keyof Sized]: C },
elementMinWidth?: number,
elementMinHeight?: number
) {
const mutEl = { ...element };
const x = mutEl[elKeys.x];
if (mutEl[elKeys.x] < 0) mutEl[elKeys.x] = 0;
if (mutEl[elKeys.y] < 0) mutEl[elKeys.y] = 0;
if (mutEl[elKeys.x] + mutEl[elKeys.width] > container[cKeys.width]) {
mutEl[elKeys.x] = container[cKeys.width] - mutEl[elKeys.width];
}
if (mutEl[elKeys.y] + mutEl[elKeys.height] > container[cKeys.height]) {
mutEl[elKeys.y] = container[cKeys.height] - mutEl[elKeys.height];
}
if (elementMinWidth !== undefined && mutEl[elKeys.width] < elementMinWidth) {
mutEl[elKeys.width] = elementMinWidth;
}
if (elementMinHeight !== undefined && mutEl[elKeys.height] < elementMinHeight) {
mutEl[elKeys.height] = elementMinHeight;
}
return mutEl;
}
interface Chair {
chairX: number;
chairY: number;
chairWidth: number
chairHeight: number;
color: string; // one extra prop that has nothing to do with dimensions
}
const exampleElement: Chair = {
chairX: -3,
chairY: 55,
chairWidth: 40,
chairHeight: 40,
color: 'black'
}
const exampleContainer = {
cW: 555,
cH: 3300
};
const containerKeys = {
width: "cW",
height: "cH"
} as const;
const chairKeys = {
x: "chairX",
y: "chairY",
width: "chairWidth",
height: "chairHeight"
} as const;
// Error: Property 'color' is missing in type '{ chairX: number; chairY: number; chairWidth: number; chairHeight: number; }'
// but required in type 'Chair'.(2741)
const fitsIntoContainer: Chair = fitElementIntoContainer(
exampleElement,
exampleContainer,
chairKeys,
containerKeys
);
I have tried many variations, but something always breaks. Is this possible?
Comments
Post a Comment