Repository navigation
Proposal: new "invalid" type to indicate custom invalid states #23689
Description
Proposal
A new invalid type that is not assignable to or from any other types. This includes not being assignable to or from any or never. It probably shouldn't even be assignable to invalid itself if that is possible, although I doubt that one really matters. I'd additionally suggest that, unlike other types, invalid | any is not reduced to any and invalid & never is not reduced to never.
The idea is to make sure that there is a compile error any time an invalid type is inferred or otherwise pops up in a users code.
invalid types would come from conditional types to represent cases where the conditional type author either expects the case to never happen, or expects that it might happen but intentionally wants that case to cause a compile error indicating to the user that something is invalid with the code they wrote.
The invalid type should also allow optionally passing an error message that would be displayed to the user when they encounter a compile error caused by the type that could give them a better idea of exactly what the problem is and how to fix it.
Motivating Examples
Allowing either true or false but not boolean - #23493 (comment)
type XorBoolean<B extends boolean> = boolean extends B ? invalid<'only literal true or false allowed'> : boolean
declare function acceptsXorBoolean<B extends boolean & XorBoolean<B>>(arg: B): void
acceptsXorBoolean(true) // allowed
acceptsXorBoolean(false) // allowed
declare const unknownBoolean: boolean
acceptsXorBoolean(unknownBoolean)
// would have error message:
// Argument of type 'boolean' is not assignable to parameter of type invalid<'only literal true or false allowed'>It's possible to write the above example today(playground link) using never instead of invalid, but it generates an error message saying: Argument of type 'boolean' is not assignable to parameter of type 'never'. which is very likely to be confusing to a user who encounters it.
Preventing duplicate keys - #23413 (comment)
type ArrayKeys = keyof any[]
type Indices<T> = Exclude<keyof T, ArrayKeys>
type GetUnionKeys<U> = U extends Record<infer K, any> ? K : never
type CombineUnion<U> = { [K in GetUnionKeys<U>]: U extends Record<K, infer T> ? T : never }
type Combine<T> = CombineUnion<T[Indices<T>]>
declare function combine<
T extends object[] &
{
[K in Indices<T>]: {
[K2 in keyof T[K]]: K2 extends GetUnionKeys<T[Exclude<Indices<T>, K>]> ? invalid<"Duplicated key"> : any
}
} & { "0": any }
>(objectsToCombine: T): Combine<T>
const result1 = combine([{ foo: 534 }, { bar: "test" }]) // allowed
const error1 = combine([{ foo: 534, dupKey: "dup1" }, { bar: "test", dupKey: "dup2" }]) // errorToday(playground link) using never instead of invalid the error message for error1 is:
Argument of type '[{ foo: number; dupKey: string; }, { bar: string; dupKey: string; }]' is not assignable to parameter of type 'object[] & { "0": { foo: any; dupKey: never; }; "1": { bar: any; dupKey: never; }; } & { "0": any...'.
Type '[{ foo: number; dupKey: string; }, { bar: string; dupKey: string; }]' is not assignable to type '{ "0": { foo: any; dupKey: never; }; "1": { bar: any; dupKey: never; }; }'.
Types of property '"0"' are incompatible.
Type '{ foo: number; dupKey: string; }' is not assignable to type '{ foo: any; dupKey: never; }'.
Types of property 'dupKey' are incompatible.
Type 'string' is not assignable to type 'never'
which would be basically impossible to understand if you didn't expect the function would reject duplicated keys. Using invalid<"Duplicated key"> however the error message could read:
Argument of type '[{ foo: number; dupKey: string; }, { bar: string; dupKey: string; }]' is not assignable to parameter of type 'object[] & { "0": { foo: any; dupKey: invalid<"Duplicated key">; }; "1": { bar: any; dupKey: invalid<"Duplicated key">; }; } & { "0": any...'.
Type '[{ foo: number; dupKey: string; }, { bar: string; dupKey: string; }]' is not assignable to type '{ "0": { foo: any; dupKey: invalid<"Duplicated key">; }; "1": { bar: any; dupKey: invalid<"Duplicated key">; }; }'.
Types of property '"0"' are incompatible.
Type '{ foo: number; dupKey: string; }' is not assignable to type '{ foo: any; dupKey: invalid<"Duplicated key">; }'.
Types of property 'dupKey' are incompatible.
Type 'string' is not assignable to type 'invalid<"Duplicated key">'
Which gives a very clear hint that the problem is that dupKey is duplicated.
Conditional cases which should never happen
I could also see invalid potentially being used for some conditional types where there is a branch that presumably never gets taken because you are just using the conditional type for the infer capability. For example at the end of #21496 there is a type:
type AnyFunction = (...args: any[]) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : never;Maybe invalid<"should never happen"> is used instead of never for the false branch so it's easier to track down the problem if it ever turns out the assumption that the branch will never be taken is wrong. (Of course if T is any, both the true and false branches are always taken so you might not want to change it away from never, but at least there'd be the option)
Related Issues
#20235 - Generics: Cannot limit template param to specific types - Could benefit from an approach like XorBoolean above.
#22375 - how do i prevent non-nullable types in arguments - Solution here is basically the same idea as XorBoolean. The error message for this specific issue is already understandable but it shows there is more interest in the pattern.
#13713 - [feature request] Custom type-error messages - Similar sounding idea, but it seems to be focused on changing the wording of existing error messages.
Search Terms
invalid type, custom error message, generic constraint, conditional types
Activity
kpdonn commentedon Apr 25, 2018
invalidcould also be a solution to the problem of thenameglobal from #18433. Currently it is typed asneverand is suggested to be changed tovoid, but neither of them is a complete solution that protects against accidental usage in every use case.invalidseems like it would prevent accidental usage in all cases though since it is defined to not be assignable to or from anything.AnyhowStep commentedon May 12, 2018
I've been playing around with conditional types a lot recently and what I currently do is,
("Some error message"|void|never)for "invalid" types.Sometimes, I'll add other types to it,
("Unexpected inferred type"|InferredType|void|never)Obviously, this is not ideal.
For one, it is not always possible to make complicated types work with the above workaround. Sometimes, you simply just have to use
never.I'd like to add an additional suggestion where it would be nice to be able to add information aside from a
stringerror message.Maybe have the
Invalid<>type be a variadic generic,Kukkimonsuta commentedon May 22, 2018
Implementation of this proposal would be a great help - combining it with conditional types and generic parameter defaults would allow for pretty precise generic constraints. It might also be worth to introduce a generic discard (
_) to make it clear that given generic parameter is only a constraint (and possibly make it impossible to override it):AnyhowStep commentedon May 22, 2018
I'm on my phone right now but I have some types I use as error states at the moment,
It works well enough for now but is unwieldy with more complicated types because it doesn't behave like
neverjcalz commentedon Jul 24, 2018
I just have to say that almost every time I use a conditional type I wish this proposal or something like it were implemented. If I could give this issue more than one 👍 I would.
bcherny commentedon Aug 13, 2018
Another use case (also, proposing a type-level
throwinstead of introducing a new type, to indicate that we're now in a bad state and should break out of normal typechecking and mark downstream values asnever):#26400
zpdDG4gta8XKpMCd commentedon Aug 13, 2018
some more duplicates:
jack-williams commentedon Sep 20, 2018
Would it be possible to view
invalidas having a different 'kind' to ordinary TypeScript types? (SayErrrather than*as per Haskell). No run-time value has a type of kindErr, so anytime it pops up in something like an argument position we'll get a type error.jack-williams commentedon Sep 24, 2018
I think it does matter, otherwise a conditional type with
invalidcould never be assignable to itself, or conditional types that are the same:both assignments would be illegal if
invalidwas not assignable to itself because assignment for conditional types is done through congruence.Kinrany commentedon Oct 31, 2018
Perhaps just having a variable with a type that allows
invalidshould produce a type error?In the example above I'd expect the signature
function foo<T>(x: Foo<T>, y: Bar<T>)to be illegal becauseTdoes not extend1.Alternatively TypeScript could automatically constrain
Tso that no variable could possibly have a type that allowsinvalid.cshaa commentedon Nov 11, 2018
This would really like to see this implemented, ideally as type-level
throw. I use really thorough type-checking and the errors often look very confusing and unintuitive.The way I handle invalid types in my code is something like this:
Finally, I'd just like to add some keywords, so that people are more likely to find the issue:
Keywords: custom compile time error, custom early error, throw type, custom invalid type, throw in type declaration, conditional type error
AnyhowStep commentedon Dec 13, 2018
I figured I'd drop my current hack-y workaround for compile-time error messages over here.
It relies on how
unknownbehaves with type intersection, and conditional types.The benefit of this workaround is that you do not pollute the return type unnecessarily.
The drawback is that calling functions that use this workaround (especially in a generic context) may be a little more tiresome.
Playground
zpdDG4gta8XKpMCd commentedon Feb 26, 2019
desperately need a special type (let's call it
invalid) that would stop type checking anyfurther if gets deduced in a attempt to resolve a generic:4 remaining items
wongjiahau commentedon May 30, 2019
Thanks a lot!
AnyhowStep commentedon May 30, 2019
Just so you know, that parameter-error trick has limitations.
For example, type inference breaks for
ReturnType<>if you use it inside the parameter list. I feel like it's a bug but gotta wait on the TS team, though.#29133 (comment)
However, there is a workaround. It's more inconvenient but works with
ReturnType<>+inference and makes the error message look cleaner. The drawback is the error span is not localized to the parameter anymore, and calling a function using the workaround is just... Ugly.#29133 (comment)
AnyhowStep commentedon Jul 17, 2019
WJH (@wongjiahau)
So, I've been playing around with compile-time errors some more and I've had the following requirements,
Compile errors must be assignable to each other.
CompileError<["errorA"]>must be assignable toCompileError<["errorB"]>This is important because it lets us change the error message between versions of our packages
and not break backwards compatibility.
It should not be possible to "legitimately" create a value of type
CompileError<>With the above requirements, my hacks using tuples
["errorA"]or union types["errorA"]|voiddon't work out.["errorA"]is not assignable to["errorB"]You can easily create a value of type
["errorA"]I've revised my hack for compile-time errors but it still has the limitation that you should not use
ReturnType<>inside of function parameter lists.So, do not use
function foo<F extends () => number> (f : F).Instead, use
function foo<N extends number> (f : () => N).Below is a revised version of the hack and examples of how it works,
Playground
The
s.select()example is the most complex code snippet but it shows that we can have pretty complex compile-time checks if we are creative enough.Also note that
typeof fooandtypeof barare assignable to each other, even though they have different error messages. This is useful so we can change error messages between versions of packages and not break backwards compatibility.A user may use both
v1.0.0andv1.1.0of your library and pass types fromv1.0.0tov1.1.0and vice versa. If we just use regular tuples, changing the error message means the error message types are no longer compatible.Native support for this would be cool.
In particular, notice that the
s.select()example has a crazzzyyyyy long error message!What we are really interested in is the
'CompileError<["Duplicate column names not allowed in select", "a"]>'part.However, we have to scroll pretty far down to see it.
When types start becoming thousands of lines long (it happens to me), this is a PITA
In my opinion, the
CompileError<>type only really makes sense in the context of function parameter lists, at the moment.If you could somehow make it so that a resolved return type of a generic function containing
CompileError<>really causes a compile error, then it would make sense to have it there, too.But such a thing would require native support for this type.
Otherwise, it would be easy for the following to happen,
If you're getting
'ErrorMessageT' is declared but its value is never read.ts(6133),just change it to
_ErrorMessageTAnyhowStep commentedon Sep 1, 2019
Another use-case for compile-time error types,
Playground
(See the last two examples)
[Edit]
I know, we already have optional chaining coming up.
But maybe the function can return the string keys joined with "." or something.
A type-safe way to build dot-separated object paths.
Also, I think I wrote the above example before coming up with the
CompileError<>workaround.You should substitute those string-tuple-as-error-message workarounds for
CompileError<["some error", "message"]>It's safer for the implementation that way
AnyhowStep commentedon Sep 15, 2019
I'll be updating this comment over the period of a few hours with my thoughts on how my ideal implementation would behave.
I'll be using the
CompileError<>syntax.Use Case
The all-important question.
In the examples that follow, demonstrating intended behaviour, the examples will all be contrived.
Just so the usefulness of this feature isn't in doubt, you may refer to this link for a list of real and complex use cases.
https://github.com/AnyhowStep/tsql/search?l=TypeScript&p=2&q=CompileError
In a project I'm working on, the
CompileError<>type is used to emulate a subset of features of the desiredinvalidtype.The library performs compile-time checks on SQL queries and ensures they are always syntactically valid and type-safe, when executed.
Intended Behavior (in my opinion),
Type Alias Behaviour
The
CompileError<>type should result in an actual compile error when instantiated concretely.In generic contexts, no compile error is given.
Conditional Type Branch Behaviour
If the conditional type can be evaluated immediately (not deferred because of a generic type param),
then we ignore
CompileError<>types in unevaluated branches.If the conditional type is deferred, then we ignore
CompileError<>on both branches.extendsBehaviourThe types
neverandCompileError<>are considered sub types of each other, for the purpose of type checking.The rationale is that the code should not compile successfully, let alone run, when a concrete custom compile error is encountered.
So, a value of type
CompileError<>should essentially never exist.Type Parameter Behaviour
The
CompileError<T>type may be used as a default type argument without triggering an actual compile error. This can be used to force users to specify an explicit type parameter.I have seen this requested over Gitter many times. Google might also somewhat benefit from it, with their return-only generics.
#33272
Because
CompileError<>is also a bottom type likenever, we can have this,Variable Behaviour
Different
CompileError<>types are assignable to each other, since they all function asnever,Property Behaviour
Parameter Behaviour
Custom compile errors at function parameters can be emulated at the moment,
but are hacky and messy.
You can see the hack in action through this link,
https://github.com/AnyhowStep/tsql/search?l=TypeScript&p=2&q=CompileError
Return Type Behaviour
At the moment, there is no workaround to enable real return type compile error behaviour.
Sample Complex Use Case
The following complex use case comes from a project I'm working on,
https://github.com/AnyhowStep/tsql/blob/e7721dbda5e99acf77a90eeb148e8ade3313fbd2/src/on-clause/util/predicate/assert-no-outer-query-used-ref.ts#L56-L72
In MySQL, the following query is invalid,
A subquery cannot reference outer query columns in the
ONclause in MySQL.The above query will result in a compile-time error,
Jack-Works commentedon Sep 6, 2020
I'm working on this with PR #40336, I think it will be super powerful.
Jack-Works commentedon Sep 6, 2020
PR at #40402
shicks commentedon Aug 25, 2022
I would love to see this move forward. As a library developer, I regularly want some way to force a type error, and the current solutions are spotty (e.g., as outlined above, there's not a good way to indicate a bad return type, and weirdly-wrapped parameters can only get you so far, and then provide awkward errors at best).
shicks commentedon Aug 29, 2022
It's possible that a small extension of the new
satisfieskeyword could go a long way to making this work. If you could writethen you could write
Foo<'abc'>becauseIsStringLiteral<'abc'>is just'abc', whileFoo<string>would give an errorHere's a more concrete motivation (playground):
I want to lock down
isEnumto only be allowed to pass in an enum container - but currently this gives an error that the type parameterThas a circular constraint, since it's trying to treatIsEnum<T>as both a lower bound (which affects inference) and a checkable constraint. Just like how the newsatisfiesis only a check (compared toas,implements, andextends, which all actually affect inference), if it were allowed in this positionthen the type checker could still check the constraint but not establish a cycle.
ryb73 commentedon Nov 11, 2022
Since a fix hasn't been pushed to TS yet, I decided to publish https://www.npmjs.com/package/invalid-type. It's inspired by the discussion in this thread and fixes some use cases I encountered myself (to be honest I only skimmed this thread, so there may be valuable contributions here that didn't make their way into my package). Feel free to contribute if you have use cases that aren't handled.
shicks commentedon Jan 30, 2023
I consider this part of a handful of related issues needed for library-friendly type checking.
I recently put together a "wish list" (see this gist) for a few features that I'd like to see in TypeScript generics, and that have some pretty solid synergy (such that any one by itself may not be particularly compelling, but when combined with the others, it makes some significant improvements).
These are clearly relevant issues that a lot of people would like to see addressed. It would be great if we could get some more eyes on them, particularly from the TypeScript maintainers.
adamscybot commentedon Apr 19, 2024
This is sorely needed. Since the introduction of Template Literal Types and the associated level of complexity that this can lead to, it's very frustrating in a lib scenario to only be able to natively present
neverwhen something does not match.I wonder whether
neveritself could be extended. For example,never<'Custom error message'>. It would have the advantage of not introducing entirely new semantics (error messaging would be restricted specifically tonever) and would be familiar, though as always with this type of thing you can easily inadvertently introduce a gazillion unforeseen gotchas.samueldcorbin commentedon Sep 18, 2024
The lack of a custom message for invalid/never is one of the biggest remaining problems in library development.
The developer experience of working with well-typed libraries in TypeScript that use practically any conditional types is just godawful. You get completely unreadable error messages, the workarounds are miserable, and the usual solution is just giving up: when someone asks for help understanding a type error from a library, usually people just shrug and tell them that they need to dig through the library's docs and hope they can find a solution because it's just accepted that complex conditional type errors are not readable by most developers.
I can't think of a TypeScript improvement that would do as much good to improve the experience of as many developers - conditional types are just miserable without it.
yaindrop commentedon Dec 21, 2024
+1 on the need of this feature. It will greatly help readability of complicated generic types.
As of now I have been using this following workaround in my project
which is unsatisfactory but still does the trick. The
neversymbol is not exported so that no user can actually build a value of theNevertype. Still it postpones the error to the use of a type instead of the definition, so an offial support for invalid type is definitely better.RobMayer commentedon Aug 7, 2025
I find myself constantly running into situations, both as consumer of libraries, and as writer of libraries, where I could throw more meaningful and helpful errors at the type level. Can we please give this some serious consideration?