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
);

Playground

I have tried many variations, but something always breaks. Is this possible?



Comments

Popular posts from this blog

Spring Elasticsearch Operations

Network Error and Timeout on Authorize.net JS

Object oriented programming concepts (OOPs)