TypeScript: Don't widen return types of function expressions

This change courtesy @JsonFreeman who is trying it out

(context elided: widening of function expression return types)

The problem with this widening is observable in type argument inference and no implicit any. For type argument inference, we are prone to infer an any, where we should not:

function f<T>(x: T, y: T): T { }
f(1, null); // returns number
f(() => 1, () => null); // returns () => any, but should return () => number

So after we get to parity, I propose we do the following. We do not widen function expressions. A function is simply given a function type. However, a function declaration (and a named function expression) introduces a name whose type is the widened form of the type of the function. Very simple to explain and simple to implement.

I’ve been told that this would be a breaking change, because types that used to be any are now more specific types. But here are some reasons why it would be okay:

  • In the places where you actually need the type to be any (because there are no other inference candidates), you would still get any as a result
  • In places where there was a better (more specific) type to infer, you’d get the better type.
  • With the noImplicitAny flag, you’d get fewer errors because there are actually fewer implicit anys

Questions:

Is a principle of design changes going forward to not switch from ‘any’ to a more precise type because it can be a breaking change?

Going with ‘not a breaking change’ here because this is unlikely to break working code, but we need to verify this.

Would this manufacture two types?

In essence, we already have two types: The original and the widened type. So by that measure this is not really a change

Has someone tried it?

Jason willing to try it out and report back

About this issue

  • Original URL
  • State: open
  • Created 10 years ago
  • Reactions: 89
  • Comments: 31 (11 by maintainers)

Commits related to this issue

Most upvoted comments

Hey everyone, I opened #40311 a couple of days ago which I think should fix most of the issues that have brought people here. If you’d like to give it a try, we have a build available over at https://github.com/microsoft/TypeScript/pull/40311#issuecomment-683255340.

Time to take this back up since it’s affecting a lot of people writing reducers

I came here by Daniel’s reference from #20859. I’m having trouble writing typesafe libraries where users can provide callbacks which should follow the type contract of the library function.

And, as shown in my examples in #20859, this affects not only generic funtions (as depicted here) but also non-generic strictly typed functions. Basically, the return of a callback function is type unsafe, always, since the callback is a variable, and any assigning of a function to a variable (of a function type) ignores the return value of the r-value.

A lot of safety is missed by the current behavior.

This issue is also present when writing GraphQL resolvers:

const resolvers: Resolvers = {
  Query: {
    someField: () => ({
      requiredField: 'some value',
      // @ts-expect-error
      optionalFieldWithTyppo: 'some value',
    })
  }
}

As we can see in this example TS is not doing its job of catching the type issue introduced by the typo on the optional field name.

Playground | Other example

AFAIK there is no real workaround (except explicitly putting the return type on the function which defeats the purpose of putting the type in the first place). Is this issue still on the radar of the TypeScript team?

Happens for .map()

interface IUser {
  id: number;
}
// No errors
const users: Array<IUser> = [].map<IUser>(() => ({
  id: 1,
  unknownProperty: 'unknownProperty',
}));

// Property 'id' is missing in type '{ unknownProperty: string; }' but required in type 'IUser'.ts(2741)
const users1: Array<IUser> = [].map<IUser>(() => ({
  unknownProperty: 'unknownProperty',
}));
console.log(users);
console.log(users1);

Hi!

Is return type widening a possible cause for the lack of errors in this case?

type T = { foo: number };
type F = () => T

// no type error
const a: F = () => ({
  foo: 1,
  bar: 2,
})

Or is it because of the covariance rule on function subtyping?

I ran into problems with some .map() calls, which seems related to the issue described.

Is this still being worked on?

Hey @DanielRosenwasser, what’s the status of your pull request? I suppose I don’t have to list any reasons for wanting this feature (there are many discussions), so I’m only curious if there’s been any progress recently

I filed #33908 based on behaviour a coworker found in NGRX where NGRX’s ActionReducer<S, A> interface has a callable signature of (state: S | undefined, action: A) => S but the widening issue allows for subtle bugs as extra keys can be provided by the reducer that don’t match the provided interface.

This may factor into whether this is a breaking change or not, given how popular NGRX is amongst the Angular community.

Also to add on, it would make it very difficult to catch typos on existing keys.

ie.

const initialState: SomeState = {
  someID: number
}

createReducer(
  initialState,
  on(someAction, (state, { id }) => {
    return {
      ...state,
      someId: id  // this typo wouldn't throw errors
    }
  }
)

I filed #33908 based on behaviour a coworker found in NGRX where NGRX’s ActionReducer<S, A> interface has a callable signature of (state: S | undefined, action: A) => S but the widening issue allows for subtle bugs as extra keys can be provided by the reducer that don’t match the provided interface.

This may factor into whether this is a breaking change or not, given how popular NGRX is amongst the Angular community.