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

Proposal: strict flag to prevent uninferrable generic types from being inferred as {} #27288

Closed
3 of 4 tasks
bcherny opened this issue Sep 23, 2018 · 12 comments
Closed
3 of 4 tasks
Assignees
Labels
Add a Flag Any problem can be solved by flags, except for the problem of having too many flags Committed The team has roadmapped this issue Experiment A fork with an experimental idea which might not make it into master Suggestion An idea for TypeScript

Comments

@bcherny
Copy link

bcherny commented Sep 23, 2018

I'm sure there an issue or discussion about this I missed, but I couldn't find it via search.

Search Terms

Generic, infer, default, {}, { }, empty, shape, object

Suggestion

Behind a new strict mode flag --strictGenericBindings, when binding concrete types to generic type parameters, if a type can't be inferred TSC should throw an error at compile time instead of defaulting to {}.

Use Cases

Let's say you have the following Promise:

const p = new Promise(resolve => resolve(42)) // Promise<{}>
p.then(r =>
  r + 4 // Error: Operator '+' cannot be applied to types '{}' and '4'.
)

This gives an error when you try to use the Promise's result. This experience isn't ideal, because the error is non-local. Looking at the code that threw that error, it's not obvious why r is {}.

If I try to fix this by adding an explicit annotate when I consume r, it works:

p.then((r: number) => {
  r + 4 // OK
})

But if I have strictFunctionTypes enabled, that breaks because {} isn't assignable to number:

p.then((r: number) => {
  r + 4 // Error: Argument of type '(r: number) => void' is not assignable to parameter of type '(value: {}) => void | PromiseLike<void>'.
})

The real fix is to explicitly bind a type parameter when I consume Promise:

const p = new Promise<number>(resolve => resolve(42)) 
p.then(r => {
  r + 4
})

But unfortunately, the error messages didn't help me get there.

A possible solution to this problem is to throw a helpful error message when a generic type parameter can't be inferred. This can help prevent bugs, and help programmers catch bugs earlier:

const p = new Promise(resolve => resolve(42)) // Error: Unable to infer a generic type for Promise<T> - please bind one explicitly. Eg. new Promise<myType>(...).
p.then(r => {
  r + 4
})

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)

Related Issues

@ajafff
Copy link
Contributor

ajafff commented Sep 23, 2018

In the meantime you can use a lint rule to detect such cases: https://github.com/fimbullinter/wotan/blob/master/packages/mimir/docs/no-inferred-empty-object.md

@ajafff
Copy link
Contributor

ajafff commented Sep 23, 2018

IIRC there was a PR for such a flag, but it was not accepted for whatever reason. I cannot find it anymore

@ajafff
Copy link
Contributor

ajafff commented Sep 23, 2018

I found it by accident: Duplicate of #5254, relevant comment: #5254 (comment)

@bcherny
Copy link
Author

bcherny commented Sep 23, 2018

@ajafff Great find! My issue is a proposed solution for the same problem raised in #5254. And it looks like @DanielRosenwasser already started working on it a while back e21f7cf.

If TypeScript authors feel like it's a good idea, I'd love to continue Daniel's work with a fully fleshed out PR.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Sep 24, 2018
@RyanCavanaugh RyanCavanaugh added the Add a Flag Any problem can be solved by flags, except for the problem of having too many flags label Oct 8, 2018
@evmar
Copy link
Contributor

evmar commented Oct 8, 2018

I think we (Google) would be interested in using this. bcherny's description about it makes errors with strictFunctionTypes non-local is exactly our experience.

@RyanCavanaugh RyanCavanaugh added Committed The team has roadmapped this issue and removed In Discussion Not yet reached consensus labels Oct 9, 2018
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.3 milestone Oct 9, 2018
@RyanCavanaugh
Copy link
Member

Daniel will see what the impact of such a flag would be

@bcherny
Copy link
Author

bcherny commented Feb 8, 2019

It's exciting to see this committed for 3.4! One more use case, for record-keeping:

function ReactComponent() {
  const [boolean, setBoolean] = useState(true) // ok
  setBoolean(false) // ok

  const [array, setArray] = useState([])
  setArray([1, 2, 3]) // Error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'SetStateAction<never[]>'.
}

@OliverJAsh
Copy link
Contributor

As an aside, why is {} used as a fallback instead of unknown?

@jack-williams
Copy link
Collaborator

@OliverJAsh relevant comment here.

@OliverJAsh
Copy link
Contributor

Another use case: when using a pipe or compose function, generics are sometimes lost.

declare const identity: <T>(a: T) => T;
declare const pipe: <A, B>(ab: (a: A) => B) => (a: A) => B;

const fn = pipe(identity); // generics lost, now {} => {}

I run into this very often when trying to compose my React HOCs:

declare const pipe: <A, B, C>(ab: (a: A) => B, bc: (b: B) => C) => (a: A) => C;

type Component<P> = (props: P) => {};

declare const myHoc1: <P>(C: Component<P>) => Component<P>;
declare const myHoc2: <P>(C: Component<P>) => Component<P>;

declare const MyComponent1: Component<{ foo: 1 }>;

// generics lost, now Component<{}>
const MyComponent2 = pipe(
    myHoc1,
    myHoc2,
)(MyComponent1);

IIUC, this particular use case concerns #12838 and #10247.

However, with the option proposed here, TypeScript would at least alert the developer to change the code to workaround this problem, to something like:

// Component<{ foo: 1 }>
const MyComponent2 = pipe(
    () => myHoc1(MyComponent1),
    myHoc2,
)({});

@vkrol
Copy link

vkrol commented May 31, 2019

Can it be closed now?
https://github.com/microsoft/TypeScript/wiki/Breaking-Changes#generic-type-parameters-are-implicitly-constrained-to-unknown
#30637

@RyanCavanaugh
Copy link
Member

We didn't quite implement this flag, but the new behavior of unknown fixes the vast majority of problems associated with {}. Combined with strictFunctionTypes and not writing generic functions which can be called with zero inference sites in the first place, I think this effectively fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Add a Flag Any problem can be solved by flags, except for the problem of having too many flags Committed The team has roadmapped this issue Experiment A fork with an experimental idea which might not make it into master Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants