Skip to content

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

New issue

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

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

Already on GitHub? # to your account

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.

# for free to join this conversation on GitHub. Already have an account? # 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