1

what I need is:

  • typescirpt understood that the first element of each internal array is a prop create (therefore with autocomplete)
  • that the second element of each internal array was typed with the object type passed (realHero) with the prop of the first parameter, therefore, if I pass ["name", ... ] typescript must accept only reader<string> since realHero["name"] is string
  • that the final result at the level of API did not change

this is my code

enum HeroSex {
  male,
  female,
  unknown,
}

interface Hero {
  name: string;
  age: number;
  sex: HeroSex;
}

type reader<T> = (value: T) => void;

//the problem is 99% here :(
type readers<T, K extends keyof T> = [prop: K, readerFn: reader<T[K]>];

const readString: reader<string> = (value: string) => {
  console.log(`the string is ${value}`);
};

const readNumber: reader<number> = (value: number) => {
  console.log(`the number is ${value.toFixed()}`);
};

const readHeroSex: reader<HeroSex> = (value: HeroSex) => {
  console.log(`the enum is ${value.toString()}`);
};

const readProperties = <T>(Obj: T, readers: readers<T, keyof T>[]) => {
  for (const [prop, reader] of readers) {
    reader(Obj[prop]);
  }
};

const realHero: Hero = {
  name: "batman",
  age: 38,
  sex: HeroSex.male,
};

//expected result (for the moment is not working)
readProperties(realHero, [
  ["age", readNumber], //ok 
  ["name", readString], //ok 
  ["sex", readNumber],  //typescript error
]);

readProperties(realHero, [
  ["age", readNumber], //ok 
  ["name", readString], //ok 
  ["sex", readHeroSex],  //ok
]);
Cosimo Chellini
  • 1,560
  • 1
  • 14
  • 20

1 Answers1

2

Please let me know if it works for you:

enum HeroSex {
   male,
   female,
   unknown,
}

interface Hero {
   name: string;
   age: number;
   sex: HeroSex;
}

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
   k: infer I
) => void
   ? I
   : never;

type UnionToOvlds<U> = UnionToIntersection<
   U extends any ? (f: U) => void : never
>;

type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;

type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
   ? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
   : [T, ...A];


type MapPredicate<T> = T extends keyof Hero ? [T, (arg: Hero[T]) => any] : never

type Mapped<
   Arr extends Array<unknown>,
   Result extends Array<unknown> = []
   > = Arr extends []
   ? []
   : Arr extends [infer H]
   ? [...Result, MapPredicate<H>]
   : Arr extends [infer Head, ...infer Tail]
   ? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
   : Readonly<Result>;
;


type Data<T> = Mapped<UnionToArray<keyof T>>

const makeData = <T,>(Obj: T, readers: Data<T>) => { };

const hero: Hero = {
   name: "batman",
   age: 38,
   sex: HeroSex.male,
};

const result1 = makeData(hero, [
   ['name', (arg: string) => null],
   ['age', (arg: number) => null],
   ['sex', (arg: HeroSex) => null]
])

const result2 = makeData(hero, [
   ['name', (arg: number) => null], // error
   ['age', (arg: number) => null],
   ['sex', (arg: HeroSex) => null]
])

Here you can find more explanations.

Credits: Shanon Jackson , Titian Cernicova-Dragomir,@jcalz

In my example, you should define callbacks for all keys, if you want to make it more generic, you can use optional operator. Just replace above Mapper util with next:

type Mapped<
   Arr extends Array<unknown>,
   Result extends Array<unknown> = []
   > = Arr extends []
   ? []
   : Arr extends [infer H]
   ? [...Result, MapPredicate<H>?] // <-- added question mark
   : Arr extends [infer Head, ...infer Tail]
   ? Mapped<[...Tail], [...Result, MapPredicate<Head>?]> // <--- added question mark
   : Readonly<Result>;
;