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 · 35 comments
Open

Don't widen return types of function expressions #241

RyanCavanaugh opened this issue Jul 24, 2014 · 35 comments
Labels
Experimentation Needed Someone needs to try this out to see what happens Fix Available A PR has been opened for this issue Suggestion An idea for TypeScript
Milestone

Comments

@RyanCavanaugh
Copy link
Member

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

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 mhegazy added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. and removed Needs More Info The issue still hasn't been fully clarified labels Jul 27, 2015
@mhegazy
Copy link
Contributor

mhegazy commented Jul 27, 2015

We need an implementation proposal for this one.

@JsonFreeman
Copy link
Contributor

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 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

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

@JsonFreeman
Copy link
Contributor

@myitcv See #138

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Aug 15, 2017
@RyanCavanaugh RyanCavanaugh removed their assignment Aug 15, 2017
@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Sep 5, 2017
@RyanCavanaugh
Copy link
Member Author

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

@EddyVinck
Copy link

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

Is this still being worked on?

@folex
Copy link

folex commented May 19, 2022

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?

@MaksimKlepikov
Copy link

MaksimKlepikov commented May 30, 2022

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);

pezy pushed a commit to pezy/studio that referenced this issue Oct 25, 2023
…6995)

**User-Facing Changes**
None

**Description**

TypeScript currently widens inferred types of function expressions
(microsoft/TypeScript#241). This results in
the following [sad
situation](https://www.typescriptlang.org/play?#code/MYewdgzgLgBAhjAvDA2gRgDQwExYMwC6A3AFAlQCeADgKYwAqSMA3iTOzAB4BcMYArgFsARjQBOpAL5k4AOkFwqACiUBLAJS9GiAHwsSAegMwAegH42HMTSj8xYfRyddeqjJecwKvAEQgQVBCqND5YMEYw4mIgYjCAvBuAsjseMNKS6qQkcgpUADz0OioaSHqsEebJ1rb2jp4uMG7JTt4wfgFBIWERYCCRYtGxgHwbgCi7yanpZEA):

```ts
const a = [1, 2, 3];

type T = {
    x: number;
}

a.map((i): T => {
    return {
        x: i,
        y: "oopsie",  // error 👍
    }
});

a.map<T>((i) => {
    return {
        x: i,
        y: "oopsie",  // no error 🤔
    }
});
```

This PR adds a lint rule to automatically replace `map<T>((i) => ...`
with `map((i): T => ...`.

Depends on https://github.com/foxglove/studio/pull/6989 to merge first.

Resolves FG-5336
@Akryum
Copy link

Akryum commented Oct 26, 2023

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?

@yamcodes
Copy link

yamcodes commented Aug 23, 2024

This issue seems to be the cause of this error:

type MyResponse = {
  my: string;
  response: string;
};

const myFunc = (param: string): MyResponse => {
  const myResponse = {
    my: 'my',
    response: 'response',
    extra: 'extra', // no error here
  };
  return myResponse;
};

const myFunc2 = (param: string): MyResponse => {
  return {
    my: 'my',
    response: 'response',
    extra: 'extra', // error here
  };
};

What are the action items here?

@ernestostifano
Copy link

Having same issue as @yamcodes

@alvis
Copy link

alvis commented Oct 6, 2024

@RyanCavanaugh @DanielRosenwasser I also noticed that when using object spread in the return statement of a function, no error is reported for additional properties that don’t exist in the target type.

For example:

✅ Type error is correctly raised here:

type MyResponse = {
  my: string;
  response: string;
};

const myFunc1 = (param: string): MyResponse => {
  return {
    my: 'my',
    response: 'response',
    extra: 'extra', // error here, as 'extra' is not part of 'MyResponse'
  };
};

❌ However, no error is raised in the following cases:

  1. When assigning the object to a variable first:
const myFunc2 = (param: string): MyResponse => {
  const myResponse = {
    my: 'my',
    response: 'response',
    extra: 'extra', // no error here, even though 'extra' is not part of 'MyResponse'
  };

  return myResponse;
};
  1. When using object spread:
const myFunc3 = (param: string): MyResponse => {
 const myResponse = {
    my: 'my',
    response: 'response',
    extra: 'extra', // no error here either
  };

  return { ...myResponse };
};

In both scenarios, TypeScript is allowing extra properties without raising an error even though it knows the final shape is inconsistent with the type annotations.

@snarbies
Copy link

snarbies commented Oct 7, 2024

Extra properties are allowed in typescript. Excess property checks only apply to the type in effect at the point of declaration (if any). This is generally considered a linting feature to catch typos, not a type correctness feature.

Outside that specific scenario, extra properties are not an error. Consider if you wanted to assign a value {a: A, B: B} to a variable typed {a: A}. The value satisfies the type.

When the constant declaration is a step removed from where the value is used, EPC does not come into play.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experimentation Needed Someone needs to try this out to see what happens Fix Available A PR has been opened for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.