Typescript: preserve exact element type when storing value in array

Viewed 158

Consider the following code with lodash merge:

  import { merge } from "lodash";
    
  type Cfg = {
        a: number
        b: number
        c: number
    };
    
  const cfg: Cfg = merge({ a: 1 }, { b: 2 }, { c: 4 });

This gives no error, because merge function preserves exact typings for all passed objects and resulting cfg is an intersection of all these types (type of cfg becomes a {a: number} & {b: number} & {c: number}, so it is compatible with Cfg ). If I pass to merge some property, that is not on Cfg, or miss some of the required properties, I will get an error.

But now, I need to do the same, but storing somewhere each of the objects beforehand.

Something like:

import { merge } from "lodash";
    
  type Cfg = {
    a: number
    b: number
    c: number
  };

  const arr: Partial<Cfg>[] = [];

  function addPartial<ArgType extends Cfg>(partial: ArgType) { arr.push(partial); };

  addPartial({ a: 1 });
  addPartial({ b: 2 });
  addPartial({ c: 3 });

  const cfg: Cfg = merge(...arr);

Now by setting explicit type of array elements to Partial<Cfg>, I lose the exact type, so the resulting type that I get for cfg will be Partial<Cfg>, which of course does not satisfy Cfg.

Is there any way to gradually add up these partials, storing them without losing their type, so that it could be proved that cfg satisfies Cfg type?

1 Answers

The short answer is not really, but specific cases have usable work arounds. You are asking for something along the lines of mutating type annotation which hasn't been implemented to the degree you are looking for.

The only way to change the type of some random field after call to arbitrary function is for that function to be a type assertion, so one option is to have a not actually used variable that is repeatedly asserted to contain the relevent fields then use it's type for the final merge: (playground link)

declare function merge<T>(...args: T[]): T;

// never actually used, just needed for types to accumulate every call to addPartial
const psuedo_cfg: {} = {}

type Cfg = {
    a: number
    b: number
    c: number
  };

const arr: Partial<Cfg>[] = [];

// the `| Cfg` is useful for intellisense, with just Pick<Cfg,K> intelisense doesn't know what keys are valid until they are actually given.
function addPartial<K extends keyof Cfg>(addition: Pick<Cfg,K> | Cfg, pp=psuedo_cfg): asserts pp is Pick<Cfg,K>{
    arr.push(addition);
}

addPartial({a: 5}, psuedo_cfg)
addPartial({b: 5}, psuedo_cfg)
// addPartial({c: 5}, psuedo_cfg)

const full_config: Cfg = merge(...arr) as typeof psuedo_cfg;
Related