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

Today Walkin 14th-Sept

Network Error and Timeout on Authorize.net JS

Spring Elasticsearch Operations