Home > Mobile >  typescript inferred type should be object but array is returned
typescript inferred type should be object but array is returned

Time:06-20

I want to write a type-safe function that takes an argument representing a prop that is a primitive or maybe a more specific object with values of the same primitive. for example t=number or t={x:number} or t={x:number,y:number}

so for example prop named grid which is number but can be specified with horizontal|vertical fields, so valid input would be grid=10 or grid={horizontal:10} or grid={vertical:10} or grid={horizontal:10,vertical:10}

a working implementation:

export type Primitive = string | number | boolean | null | undefined;
export type PossiblySpecifyAxis<T> = T | SpecifyAxis<T>;
export type SpecifyAxis<T> = { horizontal?: T; vertical?: T };

const parsePossiblyAxis = <
  Prop extends PossiblySpecifyAxis<Primitive>,
  Default extends NonNullable<Primitive> | undefined = undefined
>(
  prop: Prop,
  defaultValue?: Default
): {
  horizontal: Prop extends Primitive ? Prop : Prop extends { horizontal: infer V } ? V : Default;
  vertical: Prop extends Primitive ? Prop : Prop extends { vertical: infer V } ? V : Default;
} => {
  // does not matter
  ///////////////////////////////////////// 
  return {} as any
};
// works great
const c1 = parsePossiblyAxis(10) // => {horizontal: 10; vertical: 10;}
const c2 = parsePossiblyAxis({horizontal:5} as const,10) // => {horizontal: 5; vertical: 10;}
const c3 = parsePossiblyAxis(10,30) // => {horizontal: 10; vertical: 10;}

ts playground

Now i want to make this function more generic, so instead of specifying horizontal|vertical hardcoded i would like to except this type as a generic as well:

export type Primitive = string | number | boolean | null | undefined;
export type PossiblySpecific<Prop, Spec extends string> = Prop | { [key in Spec]: Prop };
//example: type t1 = PossiblySpecific<10,'x'|'y'> // => '10' | {x:10,y:10}

const parsePossiblySpecific = <
  Spec extends string[],
  Prop extends PossiblySpecific<Primitive, Spec[number]>,
  Default extends NonNullable<Primitive> | undefined = undefined
>(
  prop: Prop,
  fields: Spec,
  defaultValue?: Default
): {
  ////instead of 
  // horizontal: Prop extends Primitive ? Prop : Prop extends { horizontal: infer V } ? V : Default;
  // vertical: Prop extends Primitive ? Prop : Prop extends { vertical: infer V } ? V : Default;
  //// use 'Spec' generic to infer the type of the fields and construct the return type 
  [key in keyof Spec]: Prop extends Primitive ? Prop : Prop extends { [key in keyof Spec]: infer V } ? V : Default;
} => {
  // doesn't matter
  }
};

const c1 = parsePossiblySpecific(10,["horizontal","vertical"]) // => 10[] => Wrong! why this is the resulted type??

ts playground

In simple words: i am asking: why does:


// works pefectly fine: object is returned
{
  horizontal: Prop extends Primitive ? Prop : Prop extends { horizontal: infer V } ? V : Default;
  vertical: Prop extends Primitive ? Prop : Prop extends { vertical: infer V } ? V : Default;
}

// returns an array - WHY?
{
  [key in keyof Spec]: Prop extends Primitive ? Prop : Prop extends { [key in keyof Spec]: infer V } ? V : Default;
}

to see full working examples see ts-playgrounds. the js works great but the inferred type is wrong. this should be a stupid mistake but I can't find it. thanks in advance

CodePudding user response:

The first thing I would change is the return type of the function. Since Spec is a tuple, mapping over it will also result in a tuple. Instead you could map over the union of the contents of Spec.

{
  [key in Spec[number]]: Prop extends Primitive 
    ? Prop 
    : Prop extends { [key in keyof Spec]: infer V } 
      ? V 
      : Default;
}

The other problem is the inference of Spec. The way you specified the argument fields, Spec will always be infered as just string[] instead of a tuple like ["horizontal","vertical"].

To fix this, we can use the spread syntax.

const parsePossiblySpecific = <
  Spec extends string[],
  Prop extends PossiblySpecific<Primitive, Spec[number]>,
  Default extends NonNullable<Primitive> | undefined = undefined
>(
  prop: Prop,
  fields: [...Spec],
  defaultValue?: Default
)

Playground

  • Related