Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't widen return types of function expressions #241

Open
RyanCavanaugh opened this issue Jul 24, 2014 · 26 comments · May be fixed by #40311
Open

Don't widen return types of function expressions #241

RyanCavanaugh opened this issue Jul 24, 2014 · 26 comments · May be fixed by #40311

Comments

@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Jul 24, 2014

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

@DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Mar 19, 2015

Bumping this since we ran into a situation with a leaked any in our own compiler. Minimal example:

function forEach<T, U>(array: T[], callback: (element: T, index: number) => U): U {
    if (array) {
        for (let i = 0, len = array.length; i < len; i++) {
            let result = callback(array[i], i);
            if (result) {
                return result;
            }
        }
    }
    return undefined;
}


forEach([1, 2, 3, 4], () => { 
    // do stuff
    if (blah) {
        // bam! accidental any
        return undefined;
    }
});
@mhegazy
Copy link
Contributor

@mhegazy mhegazy commented Jul 27, 2015

We need an implementation proposal for this one.

@JsonFreeman
Copy link
Contributor

@JsonFreeman JsonFreeman commented Mar 22, 2016

What if we just did this:

  • Don't widen when you type a function body
  • When you widen a type, and it's a function type, you widen the return type
  • When you resolveName and it's a function expression or function declaration, you widen its type
@myitcv
Copy link

@myitcv myitcv commented Apr 8, 2016

I've arrived here via #7220. Would appreciate some guidance on whether the following is expected behaviour or not (playground):

interface G<K, R> {
    (v: K): R;
}

function F<T>(v: T): T {
    return v;
}

interface A {
    __Type: "A";
}

interface B {
    __Type: "B";
}

let a: A;
let b: B;

a = b; // OK: error as expected, B is not assignable to A
b = a; // OK: error as expected, A is not assignable to B

let x: G<A, B> = F;  // How is this possible?

It's the final assignment I cannot understand... because the compiler does not complain.

@RyanCavanaugh
Copy link
Member Author

@RyanCavanaugh RyanCavanaugh commented Apr 8, 2016

During assignment checking, type parameters are replaced with any. So the effective signature of F here is (a: any) => any

@JsonFreeman
Copy link
Contributor

@JsonFreeman JsonFreeman commented Apr 8, 2016

@RyanCavanaugh
Copy link
Member Author

@RyanCavanaugh RyanCavanaugh commented Sep 5, 2017

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

@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.8.1 milestone Jan 8, 2020
@crazyair
Copy link

@crazyair crazyair commented Jan 21, 2020

export interface Demo {
    items?: () => { demo?: string };
}

export const dd: Demo = {
    items: (): ReturnType<Required<Demo>['items']> => ({ demo: '1', str: 'error' }),
};

Playground

@pronebird
Copy link

@pronebird pronebird commented Jun 4, 2020

We've noticed the same issue after some of the reducers returned garbage. Big surprise.

function reducer(state: IState, action: IAction): IState {
  switch (action.type) {
    case 'FOOL_TYPESCRIPT':
      return {
        ...state,
        ...{ ANY_NON_EXISTING_FIELD_GETS_SWALLOWED_WITH_NO_ERROR: "BOOM" },
      };

    default:
      return state;
  }
}
@mortoray
Copy link

@mortoray mortoray commented Jun 9, 2020

This problem affects any factory function that is intended to create a specific type. (From the form of #12632 )

interface OnlyThis {
  field: number
}
type FactoryFunc = () => OnlyThis
function makeOnly(ctor: FactoryFunc) { }

const test = makeOnly(()=> ({
  field: 123,
  extra: 456, // expected to fail, but does not.
}))

There's no way to prevent makeOnly from accepting a function returning an object with excess fields. It will detect missing fields, but not excess fields. Thus the user of makeOnly will no know that their values are being ignored.

@dwelle
Copy link

@dwelle dwelle commented Jun 9, 2020

@mortoray yes, this is my main issue with this. Currently, the only workaround is to explicitly type the arrow func, which kinda defeats the purpose:

const test = makeOnly((): OnlyThis => ({
  field: 123,
  extra: 456, // error
}))
@essenmitsosse
Copy link

@essenmitsosse essenmitsosse commented Jul 20, 2020

Here is another minimal example of the problem:

const getFoo2: () => Foo =  {
	foo: {
		bar: number
	}
}  => ({
	foo: {
		bar: 1,
		buz: 2, // Doesnt throw an excessive property error
	}
})

The weird thing is, its not like the function is untyped. Changing the name of foo, or adding an excessive property to the main return object, works as expected. It only fails on the nested object.

@DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Sep 2, 2020

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 #40311 (comment).

@mrlubos
Copy link

@mrlubos mrlubos commented May 17, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Linked pull requests

Successfully merging a pull request may close this issue.