Skip to content

Type-level narrowing constraints (like as const) #53813

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
dimitropoulos opened this issue Apr 17, 2023 · 6 comments · May be fixed by #56859
Open
5 tasks done

Type-level narrowing constraints (like as const) #53813

dimitropoulos opened this issue Apr 17, 2023 · 6 comments · May be fixed by #56859
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@dimitropoulos
Copy link
Contributor

Suggestion

In userland code, you can use as const, but as a library author, I often want to provide the best possible experience, and when there's an opportunity for a literal value to be produced, I would like to infer that value for my users without them having to use (or remember to use) as const.

🔍 Search Terms

constant literals, Math.random, require as const, infer literals, readonly by default

✅ Viability 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, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Provide an ability to narrow types at the type level (i.e. without JavaScript where you can use as const).

📃 Motivating Example

const notLiteral = () => "A";

A type like this produces () => string. I can improve the situation by specifying as const, which will cause it to produce () => "A".

Note: I'm not totally sure how related this is, but I've seen > () => Math.random() > 0.5 ? "A" : "B" which produces () => "A" | "B", and it's not clear to me what the difference is from the example above

However, in the context of a library, a library author doesn't have the option to do this in the type level. Something for this use-case would be a nice improvement:

////// LIBRARY CODE
const UploadThingServerHelper = <ValidRoutes,>(
  route: {
    readonly [Route in keyof ValidRoutes]: {
      middleware: () => ValidRoutes[Route]; // how can a library author force a more specific type here?
      onUpload: (response: { metadata: ValidRoutes[Route] }) => void;
    };
  }
) => {};

////// END USER CODE
const FileRouter = UploadThingServerHelper({
  example: {
    middleware: () => "someValue", // and end-user must remember to put `as const` here to force it to return a literal
    onUpload: response => {
      response.metadata; // the result is that the type here is not as narrow as it could be for the best user experience
      //       ^?
    },
  },
});

💻 Use Cases

This is useful for libraries trying to provide a great user experience with great inferencing. It should be a backwards compatible change. There are no workarounds that I'm aware of (from the code of a library).

@MartinJohns
Copy link
Contributor

MartinJohns commented Apr 17, 2023

You can already do this:

function A<T>(val: T) {}
function B<const T>(val: T) {}

A(123) // Typed number
B(123) // Typed 123

https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#const-type-parameters

@dimitropoulos
Copy link
Contributor Author

dimitropoulos commented Apr 17, 2023

thanks for the quick reply @MartinJohns!

I'm not clear how this solves the use case: here's a playground link. Can you update the library code such that response.metadata returns "someValue" not string?

In your example you are typing an argument, but it doesn't seem to work so well if you type the return value:

Screenshot_20230417_102125

@jcalz
Copy link
Contributor

jcalz commented Apr 17, 2023

A const type parameter modifier doesn't always seem to work the way I want. In cases like that I've been falling back to the tricks mentioned in #30680, as shown in this version of your playground link.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Apr 17, 2023
@chalbert
Copy link

@dimitropoulos Reusing your example from above, here's how to narrow the type.

type Narrowable = string | number | bigint | boolean;
const UploadThingServerHelper = <ValidRoutes extends Record<string, Narrowable>>(
  route: {
    readonly [Route in keyof ValidRoutes]: {
      middleware: () => ValidRoutes[Route],
      onUpload: (response: { metadata: ValidRoutes[Route] }) => void;
    };
  }
) => {};

ts-toolbelt also has the F.Narrow util that work in a similar way.

middleware: () => F.Narrow<ValidRoutes[Route]>,

@Andarist
Copy link
Contributor

I accidentally got pointed here by @jcalz's comment here and it turns out that my PR from yesterday fixes the problem mentioned here. @dimitropoulos, could you check if that covers the whole thing you reported here?

@dimitropoulos
Copy link
Contributor Author

@Andarist I didn't run it myself, but it certainly seems like it would solve the problem!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
6 participants