Skip to content

Expose inferred type for use in type annotations #33480

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

Open
5 tasks done
ethanresnick opened this issue Sep 18, 2019 · 5 comments
Open
5 tasks done

Expose inferred type for use in type annotations #33480

ethanresnick opened this issue Sep 18, 2019 · 5 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@ethanresnick
Copy link
Contributor

ethanresnick commented Sep 18, 2019

Search Terms

infer inferred type annotation

Suggestion

When annotating the type of a variable, allow the variable to have access to the type that would've been inferred for the RHS, so that the author can build a type annation derived from that inferred value. This value could be exposed as the inferred keyword or similar.

Use Cases/Examples

// All properties in `deletable` are subject to being deleted, so we want
// the type to be a Partial of the would-have-been-inferred type.
// I don't think there's a great way to write this at the moment.
const deletable: Partial<inferred> = { a: true, b: false, c: "xyz" };

// The `mutable` property might be reassigned to, but all the other properties wont.
// So combined `Readonly` and `inferred`, with an exception for mutable.
type Legal = "initial" | "middle" | "final"
const partiallyMutable: Readonly<Omit<inferred, "mutable">> & { mutable: Legal } = { 
  x: "literally", 
  y: true,
  mutable: "initial",
  lotsOfOther: 47,
  literalPropsHere: "name"
}

// We want to verify assignability to the mapped type, to make sure that, as new 
// required keys are added, this object literal is updated. However, we also want the
// values at each key to be inferred narrowly as a literal type for use in the code that follows.
type RequiredKeys = "A" | "B" | "C"
const mustHaveAllKeys: inferred & { [K in RequiredKeys]: any } =  = {
  "A": true,
  "B": false,
  "C": "hi!"
}

// typeof mustHaveAllKeys.A should be true!

Related issues

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. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh
Copy link
Member

Duplicate #32738, others. See also #7481.

There are circularity issues that make some of the scenarios described above extremely problematic; probably best off with a helper function.

@ethanresnick
Copy link
Contributor Author

ethanresnick commented Sep 18, 2019

There are circularity issues that make some of the scenarios described above extremely problematic

Can you elaborate on this a bit? I was imagining a pretty simple process, which I think would work for all the examples above:

  1. When the annotation contains inferred, compute the type of the RHS as if there were no annotation at all.
  2. Substitute that computed type in for the inferred keyword

Duplicate #32738, others. See also #7481.

Yeah, #32738 looks like a duplicate...but since that was closed without a proposal, hopefully we can continue the discussion here. I don't really see the connection to #7481 (but haven't read that whole thread); I did search #7481's thread for the the as? operator that you mentioned in your comment on #32738, but couldn't find that anywhere...

@RyanCavanaugh
Copy link
Member

When the annotated type is a mapped type, it's reasonably straightforward for a human to puzzle it out.

For pretty much anything else, it's incredibly unclear. For example, what if the type is a conditional type that could provide a contextual type?

type Magic<T> = T extends (n: number) => void ? (n: string) => void : (n: number) => void;

// What is the type of p, and the type of k?
let p: Magic<inferred> = k => { };

Compute the type of the RHS as if there were no annotation at all.

This isn't going to work very well. You wouldn't expect an implicit any on m, for example:

type HasFoo<T> = T & { foo?: any };
const f: (n: number) => any & HasFoo<inferred> = m => {};

@ethanresnick
Copy link
Contributor Author

ethanresnick commented Sep 19, 2019

This isn't going to work very well. You wouldn't expect an implicit any on m, for example:

Honestly, I think I would be fine with an implicit any on m, to preserve a simple mental model for this feature: if you use inferred in the LHS, that means TS has to infer the type of the RHS first (seems intuitive), so you lose the LHS for contextual typing. So the example would probably be rewritten as:

const f: HasFoo<inferred> = (m: number) => {};

With that rule, your conditional type example resolves to (n: string) => void (assuming implicit any on k isn't set to be flagged as an error).

That said, maybe a more sophisticated rule could be:

  1. Substitute in unknown for inferred on the LHS to produce its type.
  2. Use that type to contextually type the RHS.
  3. Substitute in the now-finalized the type of the RHS for inferred to get the final type of the LHS; now check assignability etc.

I think that'd make your const f example work as is, and p would be inferred as (n: number) => void (which is a little weird, but it's probably not a very common case).

Alternatively, other procedures could be used for converting the LHS annotation with inferred to an annotation without inferred. For example, inferred could be replaced with unknown (per the above) except where it appears within a union type, as in const x: string | inferred = ... or const x: Something<inferred | boolean>; in those cases inferred could simply be removed from the union. This would retain a bit more information for contextual typing, i.e., the RHS of const x: string | inferred = ... would be contextually typed with string rather than with unknown (which is what would happen if inferred were replaced with unknown, and the resulting string | unknown annotation were reduced).

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Sep 19, 2019
@RodrigoRoaRodriguez
Copy link

RodrigoRoaRodriguez commented Oct 19, 2020

Normally I would not have bothered to post, since I have already found a work-around, but you seem to care a lot about the language and trying to make it more capable and suitable for enterprise use. So I decided to invest a lunch in sharing my experiences in the hope that it might help you.

Summary:



Exposing inferred type for use in type annotations is completely necessary and it is a tragedy that Typescript lacks this feature. In my experience, the vast majority of the time team-mates use an index type they actually want to refer to inferred type, but can't due to Typescript limitations. I am close to banning index types in our projects due to this.

Because:

const myConstantsObject = {
  'KEY' : {a: 1},
  'VALID-KEY-2' : {a: 2}
}

Works as expected, compiles, offers vscode IDE completion and throws types error (for example that myConstantsObject['VALID-KEY'] does not exist).

But as soon as someone tries to create a type to make sure nobody adds entries of the wrong type:

const myConstantsObject:{ [key: string]: {a: number} } = {
  'KEY' : {a: 1},
  'VALID-KEY-2' : {a: 2}
}

Then suddenly myConstantsObject['VALID-KEY'] is completely OK and cashes only at runtime while myConstantsObject.KEY, which is guaranteed to exist, no longer compiles because KEY might not exist in { [key: string]: {a: number} }.

Were I to my own post without knowing anything about this, I would suggest an enum or some other workaround that would not have worked in real life, and believe there is no real use case for inferred reference. Hence, I must unfortunately share a more complicated explanation closer to real-life problem we had, which will inevitably turn this reply into a wall of text.

In-depth context:

I wrote types for a working Javascript solution and found out that Typescript is currently not capable due to lack of inferred type reference. Here is a simplified version of the real problem that originally lead me to this issue:

const objectOfHOFs = {
   fillInMissing : (objectOfHOFs) =>  (fnArgs) =>  ({...pass(objectOfHOFs), ...fnArgs}),
   overwrite : (objectOfHOFs) =>  (fnArgs) =>  ({...pass(objectOfHOFs), ...hofArgs}),
   /** dozens more... **/
} 

Where pass is a function where the object is then looped through with a for ... of loop passing a reference to objectOfHOFs to every function as an argument.

function <A extends { [key: string]: (...args: any) => any }>pass(objectOfHOFs: A) {
  const objectOfFunctions = {} as { [K in keyof A]: ReturnType<A[K]> }
  for (const [key, hof] of Object.entries(objectOfHOFs)) {
    act[key as keyof A] = hof(objectOfHOFs)
  }
 return objectOfFunctions
}

Resulting in an object of regular (i.e. non-higher-order) functions with the same keys.

Everything works exactly as it should... Until you turn on the implicit any flag, that is, because function arguments are implicit any. Also objectOfHOFs is not actually type safe. I.e. you can add an entry of a non-compliant type. E.g. overwriting properties: objectOfHOFs. fillInMissing = null or adding entries of the wrong type is completely fine, and will silently crash production if I don't catch it on code-review.

Ideally we would want to add a type to the object of higher order functions (objectOfHOFs) rather than manually have to type every single key in the entire object higher order functions for every single argument i.e:

const objectOfHOFs: {[K in keyof inferred]: (hofArgs: inferred) => (fnArgs: argType) => ReturnType<A[K]> }  = {
   fillInMissing : (objectOfHOFs) =>  (fnArgs) =>  ({...fnArgs, ...pass(objectOfHOFs)}),
   overwrite : (objectOfHOFs) =>  (fnArgs) =>   ({...pass(objectOfHOFs), ...fnArgs}),
   /* dozens more... */
} // ✅  Concise and everything is typesafe

This would be possible and easy to do if inferred type could be referenced. Currently, however, one would have to do something like this:

const objectOfHOFs = {
   fillInMissing : (objectOfHOFs: { fillInMissing: =>  (fnArgs: argType) => ManuallyWrittenReturnType  , overwrite:  (fnArgs: argType) => ManuallyWrittenReturnType2  /* dozens more... */  }) =>  (fnArgs: argType) =>   ({...fnArgs, ...pass(objectOfHOFs)}),
   overwrite : (objectOfHOFs: { fillInMissing: =>  (fnArgs: argType) => ManuallyWrittenReturnType  , overwrite:  (fnArgs: argType) => ManuallyWrittenReturnType2  /* dozens more... */  }) =>  (fnArgs: argType) =>   ({...pass(objectOfHOFs), ...fnArgs}),
   /** dozens more... **/
} // ❌  It “works”, but it is a lot of manual effort, very verbose, still type unsafe and entries for the wrong type can still be added.

Typescript does not usually let you write self referential generic types. However, I realised I that I could circumvent this limitation by writing a helper function with a generic argument type. Nowadays, whenever I need to type anything relying on inferred type I am now forced to write a completely unnecessary function with a complex generic type and confuse the life out of everyone in our team:

const objectOfHOFs = typeAsObjectOfHOFs({
   fillInMissing : (objectOfHOFs) =>  (fnArgs) =>  ({...fnArgs, ...pass(objectOfHOFs)}),
   overwrite : (objectOfHOFs) =>  (fnArgs) =>   ({...pass(objectOfHOFs), ...fnArgs}),
   /* dozens more... */
}) // 😖  It works and it is typesafe but it is a hack that confuses my coworkers.

I hope you agree with me that 1) this is a hack, 2) that adding unnecessary implementation to circumvent the limitations of the typing system is not an ideal solution, and 3) that something should be done about this.

If you have any questions or want me to put up a minimal code-sandbox with a minimal reproduction (or the hacky function work-around) don't hesitate to ask.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants