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

Draft
wants to merge 34 commits into
base: main
Choose a base branch
from
Draft

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

Jack-Works commented Sep 10, 2020

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

@dsherret dsherret Sep 11, 2020

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

Jack-Works commented Sep 11, 2020

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

Jack-Works commented Sep 11, 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.

image

@Jack-Works
Copy link
Contributor Author

Jack-Works commented Sep 11, 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"}

@RyanCavanaugh RyanCavanaugh marked this pull request as draft Sep 11, 2020
@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

Jack-Works commented Sep 11, 2020

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

tom-sherman commented Sep 11, 2020

@Jack-Works
Copy link
Contributor Author

Jack-Works commented Sep 11, 2020

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)

@ShayDavidson
Copy link

ShayDavidson commented Mar 15, 2022

Would love to see this finalized and added in an upcoming TypeScript version. We are writing an internal, heavy type-safe, library and the inaccessible type errors are a big developer experience problem for us.

@Jack-Works
Copy link
Contributor Author

Jack-Works commented Apr 7, 2022

Have you considered how throw types should behave in unions and intersections?

Not yet. I'm expecting people will use the conditional type to do this.

@OnkelTem
Copy link

OnkelTem commented May 1, 2022

Such a cool idea.
We could use throw instead of never (where it's appropriate), making error signaling earlier, and hence debugging - more convenient. I wonder why it's taking so long to get this adopted :-/

@SwapnilSoni1999
Copy link

SwapnilSoni1999 commented May 24, 2022

throws or throw with class/type/interface support would be cool :3

@Jack-Works
Copy link
Contributor Author

Jack-Works commented May 25, 2022

throws or throw with class/type/interface support would be cool :3

Can you explain?

@Ricardo-Marques
Copy link

Ricardo-Marques commented Jun 9, 2022

Was searching for a solution to provide better error messages this morning and came across this thread. This syntax is perfect. Let me know if there's anything I can do to help move this along.

@Colisan
Copy link

Colisan commented Jun 21, 2022

+1 ☝️
What is happening to this amazing PR? It's almost 2 years old, why hasn't it been merged already? Can we help anyhow?

@ssalbdivad
Copy link

ssalbdivad commented Jun 21, 2022

As TypeScript has grown over the years and libraries interested in optimizing developer experience have begun to leverage generics in increasingly complex ways, never has become insufficient to help a user understand a problem with their types.

Currently, the best option I've found is to return a string representing an error message from a generic that normally evaluates to some unrelated type. For example, the library I'm currently working on uses TS-like syntax to create validators while also allowing 1-1 type inference, e.g.:

const user = type({
    name: {
        first: "string",
        middle: "string?",
        last: "string"
    },
    age: "number",
    browser: "'chrome'|'firefox'|'other'|null"
})

// Infer the type (exactly equivalent to the expected TS)
export type User = typeof user.infer

// Validate runtime data conforms to the type
user.check(await fetchUser())

Depending on the parameters supplied by the user, there are dozens of error messages that might help them fix their definition. As of today, I am forced to use an approach like this to convey those problems (simplified for illustrative purposes):

type ValidateStrDefinition<Def extends string> = Def extends Keyword
    ? Def
    : Def extends Expression
    ? ValidateExpression<Def>
    : `Unknown type '${Def}'.`

For a call expression like model("string|nuber"), this results in an error message like "string|nuber" is not assignable to "Unknown type 'nuber'." While this is certainly clearer than ...is not assignable to never., using a throw type would:

  • Avoid the IDE suggesting users change their definition to match the error message (which would not be a valid definition anyways as the generic would be reevaluated and likewise fail to parse)
  • Standardize type error handling (hopefully at a higher level I could check if a subresult was a throw type)
  • Make it clearer for both users and developers working on the project what was going on

My fear is that given the scope of this PR, if it is not prioritized soon, it won't be tenable to maintain and will have to be rewritten.

As others have said, if there is anything I can do to help ensure this receives whatever feedback it still needs and can be merged in the near future, please let me know!

@Jack-Works
Copy link
Contributor Author

Jack-Works commented Jun 22, 2022

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

Tschrock commented Jul 10, 2022

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

Jack-Works commented Jul 10, 2022

@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

nopeless commented Sep 25, 2022

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

trevorade commented Oct 20, 2022

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)

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