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

Throw types #40468

Closed
wants to merge 34 commits into from
Closed

Throw types #40468

wants to merge 34 commits into from

Conversation

Jack-Works
Copy link
Contributor

@Jack-Works Jack-Works commented Sep 10, 2020

Fixes #23689

Playground: https://www.staging-typescript.org/play?ts=4.2.0-pr-40468-44

This PR introduces:

  1. A new type-level expression: throw type_expr. Currently throw type only throws when it is being instantiated.
  2. A new intrinsic type TypeToString to print a type.

Considered use cases:

Welcome to suggest more use cases!

TypeAlias instantation

type MustNumber<T> = T extends number ? T : throw `Expected, but found "${TypeToString<T>}"`
type A = MustNumber<1>
type B = MustNumber<typeof window>

image

Prevent CallExpression

function checkedDivide<T extends number>(x: T): T extends 0 ? throw 'Cannot divided by 0' : number {
    if (x === 0) throw new Error('')
    return 5 / x
}
checkedDivide(0)
checkedDivide(1)

const theAnswerToEverything = <T>(x: T): T extends 42 ? T : throw "Wrong" => x
theAnswerToEverything(42 as const)
theAnswerToEverything('')

function checkParameterPosition<T extends number>(y: T extends 1234 ? throw 'No zero' : T) { }
checkParameterPosition(1234)
checkParameterPosition(12345678)

image

Prevent use of identifiers

image

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Sep 10, 2020
@Jack-Works Jack-Works mentioned this pull request Sep 10, 2020
@orta
Copy link
Contributor

orta commented Sep 10, 2020

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 10, 2020

Heya @orta, I've started to run the tarball bundle task on this PR at d69f2b0. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 10, 2020

Hey @orta, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/84753/artifacts?artifactName=tgz&fileId=458CBB664D3CE9EFA165B438F01414D55349C27455F3F7CE2CC288A173FC410B02&fileName=/typescript-4.1.0-insiders.20200910.tgz"
    }
}

and then running npm install.


There is also a playground for this build.

@Jack-Works
Copy link
Contributor Author

Jack-Works commented Sep 10, 2020

Idea: Find a way to "match" the error string into the typescript diagnostic object, to reuse the translation of diagnostic message.

A possible solution:

throw {diagnostic: "Did_you_mean_0", args: [T] }

That will match the Diagnostic.Did_you_mean_0 and therefore it will be automatically translated by the compiler.

@Jack-Works
Copy link
Contributor Author

Jack-Works commented Sep 10, 2020

Idea: Find a way to emit other kinds of diagnostic, like suggestions or warning. If so, there should be another mechanism to offer a underlying type like

throw {message: T, type: T2, kind: "suggestion"}

@Jack-Works
Copy link
Contributor Author

Oh I just realized we can throw a object type so it can include more details! Please help me to investigate the possibility of this to find a good shape of the error object.

Possible properties:

  • underlying type (what should this throw type resolve to)
  • diagnosis level (error, suggestions, warning, deprecation)
  • reference to typescript built in diagnosis message to provide translated message
  • provide refactor suggestions

@@ -3032,6 +3032,10 @@
"category": "Error",
"code": 2793
},
"Type instantiated results in a throw type saying: {0}": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something shorter? Type instantiation threw. {0}?

@Jack-Works
Copy link
Contributor Author

Idea: Allow throw type to be handled by conditional type. This allows some error recovery or composing multiple errors.

T extends throw infer E1 ?
    U extends throw infer E2 ?
        throw `\n    ${E1}\n    ${E2}`
    : T : never

@Jack-Works
Copy link
Contributor Author

Jack-Works commented Sep 11, 2020

import type { DiagnosticCategory } from 'typescript'
/**
 * Since this is a type-level thing, any union in this type is considered invalid value
 * cause their value is not determinate yet.
 * 
 * `throw "string"` is convert to `throw { message: "string" }`
 */
type ErrorMessage = {
    /**
     * ! implemented !
     * If diagnostic is not exist or not valid (key not found / not a tuple) this message will be used
     * 
     * If it is not a string literal type, it will be formatted by `getTypeNameForErrorDisplay`
     */
    message: any
    /**
     * What type should this throw type compatible(equal) with?
     * Useful to "add" diagnostic message on a type and preserve itself
     * @default never
     */
    type?: any
    /**
     * ! implemented !
     * TODO: provide completion for this in the language service
     * Actually it is `[keyof Diagnostic (a @internal variable of ts), ...any[]]`
     * @example ["_0_expected", T] results in "T expected" (with translation in different languages)
     */
    diagnostic?: [type: string, ...args: any[]]
    /**
     * ! implemented !
     * @default `Error` when undefined. `Message` when value is invalid.
     */
    category?: 'suggestion' | 'error' | 'warning' | 'message'
    /**
     * It seems like deprecated is not in the DiagnosticCategory
     */
    deprecated?: boolean

    // ! Let developers custom the error code might not a good idea.
    // code: number

    /**
     * When it happened on an identifer, replace the identifier with the suggestion
     * e.g.: name => window.name
     * 
     * When it happened on a type alias, do nothing
     * When it happened on a CallExpression, do nothing
     * 
     * ? Is this really useful cause the throw cannot get the original source text ?
     * 
     * If we have higher kind types, this option can receive a un-instantiated generic type
     * as a type-level function.
     * 
     * @example
     * type MyError<Context extends ...> = ...
     * type T<U> = ... extends ... ? ... : throw {message: ..., suggestion: MyError}
     */
    suggestion?: string

    /**
     * Let message be able to chain
     */
    next?: ErrorMessage | ErrorMessage[]
}

@Jack-Works
Copy link
Contributor Author

Idea: Find a way to "match" the error string into the typescript diagnostic object, to reuse the translation of diagnostic message.

A possible solution:

throw {diagnostic: "Did_you_mean_0", args: [T] }

That will match the Diagnostic.Did_you_mean_0 and therefore it will be automatically translated by the compiler.

image

@Jack-Works
Copy link
Contributor Author

Idea: Find a way to emit other kinds of diagnostic, like suggestions or warning. If so, there should be another mechanism to offer a underlying type like

throw {message: T, type: T2, kind: "suggestion"}

@RyanCavanaugh RyanCavanaugh marked this pull request as draft September 11, 2020 07:10
@Harpush
Copy link

Harpush commented Sep 11, 2020

To be honest the initial idea is great - but adding diagnostics types and formatting feels like an overkill. It might even step into the linters world. The syntax is more complex too. I think having the ability to throw is already great as is.

@Jack-Works
Copy link
Contributor Author

The syntax is more complex too.

Hmm, I didn't introduce a new syntax for a "detailed" throw. It's a plain object literal type. I need some advice from the TypeScript team. If they think it's no need to do this, I'll stop working on that feature sets and focus on what will be accepted.

Now I still have two complex ideas:

Is there anything I mentioned above that the TS team doesn't want? cc @RyanCavanaugh @orta @Kingwl

@tom-sherman
Copy link

@Jack-Works
Copy link
Contributor Author

Oh, I'm sorry it's a regression (bug) @tom-sherman I'll fix it soon. You can try an old version https://www.staging-typescript.org/play?ts=4.1.0-pr-40402-15 (but it has other bugs)

@Jack-Works
Copy link
Contributor Author

Once I get the guide (code review or design) from the TypeScript team, I can continue on this PR. Without their input, there isn't much I can do because the experiment is basically done.

@Tschrock
Copy link

Is the playground link for this up to date?
I was hoping this could be used as a replacement for never when I know an overridden method will throw, but using it seems to cause assignability problems:

class Foo {
  testA(): number { return 123 }
  testB(): number { return 123 }
}

class Bar extends Foo {
  testA(): never { throw new Error("boop") }
  testB(): throw "my error" { throw new Error("boop") } // Type 'never' is not assignable to type 'number'.
}

const bar1 = new Bar().testA() // never
const bar2 = new Bar().testB() // Type instantiated results in a throw type saying: my error

@Jack-Works
Copy link
Contributor Author

@Tschrock your case is interesting. currently throw type is not assignable to never type, I will revisit it next time I change this PR.

bitgopatmcl added a commit to bitgopatmcl/api-ts that referenced this pull request Jul 19, 2022
Attempts to produce a helpful error message when invalid codecs are passed to `httpRequest`.
It is a workaround until something like microsoft/TypeScript#40468 is merged.
@shicks
Copy link
Contributor

shicks commented Aug 25, 2022

I'm excited to see that this is still being (somewhat) actively developed. As a library developer, I would absolutely love to see this land. Any idea how to get the necessary attention for this to move forward?

@nopeless
Copy link

It always excites me when you are looking for a feature and some devs have been working on it for years. Yes I would love a "throw" type because some type checks require extraneous chaining of type generics and it can be hard to deliver semantic reasoning

@ssalbdivad
Copy link

ssalbdivad commented Sep 25, 2022

@DanielRosenwasser Is it possible to get a quick update on the team's plans for this PR? It has received a lot of interest and support from the community and been open for over 2 years now. The author recently stated:

Once I get the guide (code review or design) from the TypeScript team, I can continue on this PR. Without their input, there isn't much I can do because the experiment is basically done.

While his diligence is admirable, he's kind of in a difficult spot having to continually make updates like this without any assurances as to when his work will be merged, if ever.

@Jack-Works Just wanted to thank you for continuing to maintain this for so long!

@trevorade
Copy link

Just pointing out that the https://github.com/mmkal/expect-type TS type-checking library could really benefit from this PR if merged.

@Jack-Works Thank you for creating this! Excited to hopefully use it!

@papb
Copy link

papb commented Oct 20, 2022

This is super nice. I have some questions - how (if) would the following constructs work?

type T = throw "oops";
type U = "foo" | throw "oops";
type V = { a?: throw "oops" };
type W = (arg: throw "oops") => any;
type X<T> = T extends throw infer E ? true : false;

Also, if I have type MakeThrow<T extends string> = throw T; can I use MakeThrow<"oops"> everywhere I could use throw T and it will work in the same way?

Also, if throw X is just another type, how does it relate to other common types? For example, does it extend never, any, unknown?

I would suggest making throw "something" extend (i.e. be a subtype of) never. In a way that never becomes the "union of all throw types".

What does everyone think? I suspect having good answers to all these questions is a way to help the PR move forward.

@shicks
Copy link
Contributor

shicks commented Oct 21, 2022

@papb (rearranging your questions slightly)

This is super nice. I have some questions - how (if) would the following constructs work?
What does everyone think? I suspect having good answers to all these questions is a way to help the PR move forward.

If you look at the files changed in the PR, the tests show what the currently-proposed behavior is. That said, maybe @orta could ask the bot to set this up so that you could try them out in the playground?

My understanding (of what at least I'd want) is that error T would give an immediate error (and resolve to never) and time T is an actual concrete type without any type variables.

type T = throw "oops";
type U = "foo" | throw "oops";
type V = { a?: throw "oops" };
type W = (arg: throw "oops") => any;

I would hope all of these would immediately error.

type X<T> = T extends throw infer E ? true : false;

This seems maybe problematic. I can see some value to being able to introspect the errors (particularly for library API testing), but I'd be inclined to move forward without it and really just make it an exact never after issuing the diagnostic. If there's a compelling use for this, it could be done in a future iteration.

Also, if I have type MakeThrow<T extends string> = throw T; can I use MakeThrow<"oops"> everywhere I could use throw T and it will work in the same way?

I would hope so. In MakeThrow's definition, you're throwing a type variable, so it shouldn't error yet. But once you instantiate that type with a concrete string, then it can produce an actual diagnostic.

Also, if throw X is just another type, how does it relate to other common types? For example, does it extend never, any, unknown?

TypeScript already treats type mismatches in a particular way, e.g. if you write declare function foo<T>(arg1: T, arg2: T): T then foo(42, 'x'); will produce a diagnostic, but type checking will continue with treating the erroneous expression as if were any. This feature should be consistent with that.

I would suggest making throw "something" extend (i.e. be a subtype of) never. In a way that never becomes the "union of all throw types".

I don't think "subtype of never" is really a concept we want to open up here. The whole point of never is that it's a subtype of everything, so I think "there be dragons".

@somebody1234
Copy link

somebody1234 commented Oct 22, 2022

type X<T> = T extends throw infer E ? true : false;

i'd assume that's intended to behave as a type level try-catch

(of course, definitely a good idea to figure out how useful it would actually be in practice)

@Methrat0n
Copy link

Just wanted to bump this up.
I'm also, like everyone else here it seams, working on a library that would greatly benefit from this.
Maybe not a fan of the 'throw' syntax and any more complicated thing than just stopping and showing a compilation error when the type is instantiated as it could create a lot of try catch hell that I've known of to well in Java.
But still, personalising error message for complexe cases is a much feature, I'm sad to see that's it's been so long.

@shicks
Copy link
Contributor

shicks commented Nov 19, 2022

My thought is that by the time there's something to "catch", it's already too late - the error has been emitted. Using throw for this is potentially nice because it's been a JS keyword forever, so it's unlikely to clobber any actual existing names. But I dislike any analogy it might make what would suggest to anyone that they can catch it to suppress any error, or really do any sort of logic. Just emit the error and bail out with a never.

@trevorade
Copy link

Agree that catching it in typing doesn't make sense.

It's a little surprising for me to think about it being recoverable in conjunction with a @ts-ignore. I want to say that @ts-ignore generally resolves weird things to any rather than never. So maybe it would make sense for throw to resolve to an any? If not suppressed, it would just stop compiling anyways.

@Jack-Works
Copy link
Contributor Author

close for housekeeping.

I still have interest in this feature, so I will re-open and rebase it if the TS team also has an interest in it and give some suggestions of its type behavior.

thanks for everyone watching & testing this PR!

@Jack-Works Jack-Works closed this Aug 9, 2023
@ssalbdivad
Copy link

ssalbdivad commented Aug 9, 2023

@Jack-Works Your work is very much appreciated!

Wanted to take the chance to underscore that since this PR was created in 2020, the most popular libraries in the ecosystem have invested even further in advanced TS features to create the rich DX many devs have come to expect.

Generics that perform some kind of input validation are a common manifestation of this trend, and unfortunately, most library authors end up resorting to TS's builtin solution, never, which is totally opaque when it comes to helping end users resolve the underlying problem.

While some libraries like ArkType have started returning custom error messages instead, the problems I mentioned in my original comment still apply, and the fact that it feels like a hack likely deters a lot of authors from returning anything other than never.

If there is something about this solution in particular that is unsatisfactory, it would be really useful to at least have an idea of what it is so that going forward contributors can avoid repeating it. I can't help but feel a bit sad seeing high-quality contributions like this languish. Everyone involved would greatly benefit from even a very brief explanation of the team's stance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Proposal: new "invalid" type to indicate custom invalid states