BizzyBob
BizzyBob

Reputation: 14750

TypeScript: Is it possible to restrict arrow function parameter return type?

I have a typescript function that accepts two params, a config object and a function:

function executeMaybe<Input, Output> (
  config: { percent: number },
  fn: (i: Input) => Output
): (i: Input) => Output | 'maybe next time πŸ˜›' {

  return function(input: Input) {
    return Math.random() < config.percent / 100 
      ? fn(input)
      : 'maybe next time πŸ˜›';
  };
}

Usage of the function looks like this:

interface Foo { number: number; }
interface Bar { string: string; }

const makeBarMaybe = executeMaybe<Foo, Bar>(
  { percent: 75 },
  foo => ({ string: `derived from Foo: ${JSON.stringify(foo)}` })
);

If I attempt to add a nonexistent property to my config object literal, TypeScript kindly informs me:

Argument of type '{ percent: number; baz: string; }' is not assignable to parameter of type '{ percent: number; }'. Object literal may only specify known properties, and 'baz' does not exist in type '{ percent: number; }'. (2345)

However, when I add an additional property to the object returned from the function param, no error is mentioned:

screenshot

Is it possible to have TS provide an error in this scenario without explicitly providing the function's return type?

another screenshot

Here's a StackBlitz where I was playing around with this.

Upvotes: 1

Views: 432

Answers (1)

You see this error because of excess-property-checks. Literal objects are treated in a differen way.

Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the β€œtarget type” doesn’t have, you’ll get an error:

If you still want to add more extra properties to your argument, you should expect a subtype of Foo instead of super type. I mean, that your argument should extend Foo instead of being Foo.

You need to rewrite your executeMaybe and apply a constraint where callback function expects subtype of Input instead of Input itself:

function executeMaybe<Input, Output>(
  config: { percent: number },
  fn: <Inp extends Input>(i: Inp) => Output
): <Inp extends Input>(i: Inp) => Output | 'maybe next time πŸ˜›' {
  return function (input: Input) {
    const result =
      Math.random() < config.percent / 100 ? fn(input) : 'maybe next time πŸ˜›';

    console.log(result);

    return result;
  };
}

WHole code:

function executeMaybe<Input, Output>(
  config: { percent: number },
  fn: <Inp extends Input>(i: Inp) => Output
): <Inp extends Input>(i: Inp) => Output | 'maybe next time πŸ˜›' {
  return function (input: Input) {
    const result =
      Math.random() < config.percent / 100 ? fn(input) : 'maybe next time πŸ˜›';

    console.log(result);

    return result;
  };
}

console.clear();

interface Foo {
  number: number;
}

interface Bar {
  string: string;
}

const makeBarMaybe = executeMaybe<Foo, Bar>({ percent: 75 }, (foo) => ({
  string: `derived from Foo: ${JSON.stringify(foo)}`,
}));

const explicitFoo: Foo = { number: 10 };
const implicitFoo = { number: 20 };
const superFoo = { number: 30, extra: 'super' };

makeBarMaybe(explicitFoo);
makeBarMaybe(implicitFoo);
makeBarMaybe(superFoo); // same object | duck typing allows
makeBarMaybe({ number: 30, extra: 'super' }); // ok

Playground

UPDATE

My bad, probably did not understand the question.

TypeScript does not support exact types, but there is a workaround:

type PseudoExact<T, U> = T extends U ? U extends T ? U : never : never

function executeMaybe<Input, Output>(
  config: { percent: number },
  fn: <Inp extends Input>(i: PseudoExact<Input, Inp>) => Output
): <Inp extends Input>(i: PseudoExact<Input, Inp>) => Output | 'maybe next time πŸ˜›' {
  return function <Inp extends Input>(input: PseudoExact<Input, Inp>) {
    const result =
      Math.random() < config.percent / 100 ? fn(input) : 'maybe next time πŸ˜›';

    console.log(result);

    return result;
  };
}

PseudoExact - checks whether supertype extends subtype.

WHole code with expected errors:

type PseudoExact<T, U> = T extends U ? U extends T ? U : never : never

function executeMaybe<Input, Output>(
  config: { percent: number },
  fn: <Inp extends Input>(i: PseudoExact<Input, Inp>) => Output
): <Inp extends Input>(i: PseudoExact<Input, Inp>) => Output | 'maybe next time πŸ˜›' {
  return function <Inp extends Input>(input: PseudoExact<Input, Inp>) {
    const result =
      Math.random() < config.percent / 100 ? fn(input) : 'maybe next time πŸ˜›';

    console.log(result);

    return result;
  };
}

console.clear();

interface Foo {
  number: number;
}

interface Bar {
  string: string;
}

const makeBarMaybe = executeMaybe<Foo, Bar>({ percent: 75 }, (foo) => ({
  string: `derived from Foo: ${JSON.stringify(foo)}`,
}));

const explicitFoo: Foo = { number: 10 };
const implicitFoo = { number: 20 };
const superFoo = { number: 30, extra: 'super' };

makeBarMaybe(explicitFoo);
makeBarMaybe(implicitFoo);
makeBarMaybe(superFoo); // expected error
makeBarMaybe({ number: 30, extra: 'super' }); // expected error

Playground

Instead of my naive PseudoExact you can use more advanced type utility. Please see this comment

export type Equals<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false;

Upvotes: 1

Related Questions