0

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?

1 Answer 1

1

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
});
Sign up to request clarification or add additional context in comments.

3 Comments

Amazing! I need to get better at TypeScript. Only issue is ReturnType seems to ignore undefined; I'd have expected ReturnType<() => string | undefined> to result in string | undefined?
@riscarrott are you sure you have strict null checks on ?
Surprisingly no I don't, works perfectly with strict null checks on, thanks you!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.