Richard Scarrott
Richard Scarrott

Reputation: 7053

TypeScript mapped types inference not working as expected

Given this function:

export const combineValidators = <Input extends { [P in keyof Input]: (val: string) => Err }, Err>(
  validators: Input
) => (values: { [P in keyof Input]?: unknown }): { [P in keyof Input]: Err } => {
  // Ignore implementation.
  return {} as { [P in keyof Input]: Err };
};

And this usage:

const validator = combineValidators({
  name: (val) => val ? undefined : 'error',
  email: (val) => val ? undefined : 'error'
});

const errors = validator({
  name: 'Lewis',
  email: '[email protected]'
});

I would expect TypeScript to be able to infer the return type as:

// Expected: `errors` to be inferred as:
interface Ret {
  name: string | undefined;
  email: string | undefined;
}

However it's inferred as:

// Actual: `errors` inferred as:
interface Ret {
  name: {};
  email: {};
}

I've created a live example in the TypeScript playground demonstrating the issue.

Can anybody help?

Upvotes: 0

Views: 117

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249706

Err will not be inferred in the way you expect it. It might be simpler to use the ReturnType conditional type to extract the return types from Input:

type ReturnTypes<T extends Record<keyof T, (...a: any[]) => any>> = {
  [P in keyof T]: ReturnType<T[P]>
}

export const combineValidators = <Input extends Record<keyof Input, (val: unknown) => any>>(
  validators: Input
) => (values: Record<keyof Input, unknown>): ReturnTypes<Input> => {
  return {} as ReturnTypes<Input>;
};

const validator = combineValidators({
  name: (val) => val ? undefined : 'error',
  email: (val) => val ? undefined : 'error'
});

const errors = validator({
  name: 'Lewis',
  email: '[email protected]'
});

We can even go a bit further and if you specify parameters types in the validator function, you can get type checking for the fields of the object passed to validator:

type ParamTypes<T extends Record<keyof T, (a: any) => any>> = {
  [P in keyof T]: Parameters<T[P]>[0]
}

type ReturnTypes<T extends Record<keyof T, (...a: any[]) => any>> = {
  [P in keyof T]: ReturnType<T[P]>
}

export const combineValidators = <Input extends Record<keyof Input, (val: unknown) => any>>(
  validators: Input
) => (values: ParamTypes<Input>): ReturnTypes<Input> => {
  return {} as ReturnTypes<Input>;
};

const validator = combineValidators({
  name: (val: string) => val ? undefined : 'error',
  email: (val) => val ? undefined : 'error', // if we leave it out, we still get unknown
  age: (val: number) => val ? undefined : 'error'
});

const errors = validator({
  name: 'Lewis',
  email: '[email protected]',
  age: 0
});

const errors2 = validator({
  name: 'Lewis',
  email: '[email protected]',
  age: "0" // type error
});

Upvotes: 1

Related Questions