Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

TypeScript Generics Problem using "infer" when a type is deduced from function argument

In the process of learning TypeScript I’m trying to create a function that will deduce the return value from the input parameter as a dictionary with values defined as function objects with propper types.

Originally this is taken from frida types library.

I am attempting to extend with a feature to define an API of functions.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

The problem I’m facing which I can ilustrate in a small snippet is that when passing the type directly to another generic type the infer keyword seems to work, but when the type is deduced from the function argument everything breaks.

I have the following code snippet with minimum code to illustrate the problem

declare class Pointer {}

type NativeTypesMap = {
  int: number;
  long: number;
  void: void;
  pointer: Pointer;
};

type ValuesOf<T> = T[keyof T];
type KeysOf<T> = keyof T;

type NativeTypesValue = ValuesOf<NativeTypesMap>;
type NativeTypesType = KeysOf<NativeTypesMap>;

type GetValue<Map, Type, T extends Type> = T extends keyof Map ? Map[T] : never;

type GetArgValue<T extends NativeTypesType> = GetValue<
  NativeTypesMap,
  NativeTypesType,
  T
>;

type Def<T extends NativeTypesType> = { r: T };

type DefAny = Def<any>;

declare type BaseApi = {
  [k: string]: DefAny;
};

type ApiCalls<T extends BaseApi> = {
  [p in keyof T]: T[p] extends Def<infer R>
    ? (...args: any) => GetArgValue<R>
    : never;
};

function createapi<T extends BaseApi>(params: T): ApiCalls<T> {
  // implementation omitted
  return {} as ApiCalls<T>;
}

type z = ApiCalls<{
  open: { r: "int" };
  malloc: { r: "pointer" };
  free: { r: "void" };
}>;
/*
 * z is calculated to be
 * type z = {
 *     open: (...args: any) => number;
 *     malloc: (...args: any) => Pointer;
 *     free: (...args: any) => void;
 * }
 */

var a = createapi({
  open: { r: "int" },
  malloc: { r: "pointer" },
  free: { r: "void" },
});
type c = typeof a;
/*
 * calculated type of a is:
 * tyoepf a = {
 *     open: never;
 *     malloc: never;
 *     free: never;
 * }
 */

TS Playground

my goal is to change createapi such that the dedcued type of a would be the same as z.

Anyone has a clue what am I missing here?

>Solution :

The problem is because of the type of the object that you are passing into the function:

// const obj: {
//   open: {
//       r: string;
//   };
//   malloc: {
//       r: string;
//   };
//   free: {
//       r: string;
//   };
// }
const obj = {
  open: { r: 'int' },
  malloc: { r: 'pointer' },
  free: { r: 'void' },
}

The compiler widens the type to primitives, to make our life easier when we are working with variables. Since obj no longer has literal values in its properties, your infer condition doesn’t work anymore.

There are two approaches to fix the problem:

const assertion – prevents typescript from widening the type:

// const obj: {
//   readonly open: {
//       readonly r: "int";
//   };
//   readonly malloc: {
//       readonly r: "pointer";
//   };
//   readonly free: {
//       readonly r: "void";
//   };
// }
const obj = {
  open: { r: 'int' },
  malloc: { r: 'pointer' },
  free: { r: 'void' },
} as const

Testing:

// {
//   readonly open: (...args: any) => number;
//   readonly malloc: (...args: any) => Pointer;
//   readonly free: (...args: any) => void;
// }
var a = createapi({
  open: { r: 'int' },
  malloc: { r: 'pointer' },
  free: { r: 'void' },
} as const);

Looks good, however, it can get quite annoying and inconvenient to add as const to every function invocation. And in Typescript 5.0 the const type parameters were added, which are doing the same thing what const assertion does over the function parameters:

function createapi<const T extends BaseApi>(params: T): ApiCalls<T> {
  // implementation omitted
  return {} as ApiCalls<T>;
}

// {
//   readonly open: (...args: any) => number;
//   readonly malloc: (...args: any) => Pointer;
//   readonly free: (...args: any) => void;
// }
var a = createapi({
  open: { r: 'int' },
  malloc: { r: 'pointer' },
  free: { r: 'void' },
});
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading