-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Proposal: new "invalid" type to indicate custom invalid states #23689
Comments
|
I've been playing around with conditional types a lot recently and what I currently do is,
Sometimes, I'll add other types to it,
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 I'd like to add an additional suggestion where it would be nice to be able to add information aside from a Maybe have the
|
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 ( interface AcceptRequiredValue<
T,
_ = undefined extends T ? invalid<'undefined is not allowed'> : never,
_ = null extends T ? invalid<'null is not allowed'> : never
> {
value: T;
} |
I'm on my phone right now but I have some types I use as error states at the moment, //Just using `Error` as an invalid type marker but it's just an ugly hack
type Invalid1<T0> = [T0]|void|Error;
type Invalid2<T0, T1> = [T0, T1]|void|Error;
type Invalid3<T0, T1, T2> = [T0, T1, T2]|void|Error;
/*Snip*/ It works well enough for now but is unwieldy with more complicated types because it doesn't behave like |
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. |
Another use case (also, proposing a type-level function head<
T extends any[],
R = T[0] extends never
? throw 'Cant read head of empty array'
: T[0]
>(array: T): R {
return array[0]
}
let a = head([1,2,3]) // number
let b = head([]) // Error: Cant read head of empty array
let c = b + 5 // never |
Would it be possible to view |
I think it does matter, otherwise a conditional type with type Foo<T> = T extends 1 ? string : invalid<"some message">;
type Bar<T> = T extends 1 ? string : invalid<"some other message">;
function foo<T>(x: Foo<T>, y: Bar<T>) {
x = y;
// ^ Error
// Type 'Bar<T>' is not assignable to type 'Foo<T>'.
// Type 'invalid<"some other message">' is not assignable to type 'invalid<"some message">'.
x = x;
// ^ Error
// Type 'Foo<T>' is not assignable to type 'Foo<T>'.
// Type 'invalid<"some message">' is not assignable to type 'invalid<"some message">'.
} both assignments would be illegal if |
Perhaps just having a variable with a type that allows In the example above I'd expect the signature Alternatively TypeScript could automatically constrain |
This would really like to see this implemented, ideally as type-level The way I handle invalid types in my code is something like this: const StringExpected = {
'TypeError: At least one of the arguments has to be string': Symbol()
};
function foo<T extends any[]>(
...args: UnionToIntersection<T[number]> extends string
? T
: typeof StringExpected[]
) {}
foo(4, Symbol(), {}); // [ts] Argument of type '4' is not assignable to parameter of type '{ 'TypeError: At least one of the arguments has to be string': symbol; }'.
foo(4, Symbol(), ''); // OK 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 |
I figured I'd drop my current hack-y workaround for compile-time error messages over here. It relies on how 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. type ErrorCheck<T extends number> = (
Extract<42|69|1337, T> extends never ?
unknown :
["You cannot use", T, "as an argument"]
);
declare function foo<T extends number>(
n : T & ErrorCheck<T>
): string
/*
Argument of type '42' is not assignable to parameter of type '42 & ["You cannot use", 42, "as an argument"]'.
Type '42' is not assignable to type '["You cannot use", 42, "as an argument"]'.
*/
foo(42);
//OK
foo(32);
/*
Argument of type 'number' is not assignable to parameter of type 'number & ["You cannot use", number, "as an argument"]'.
Type 'number' is not assignable to type '["You cannot use", number, "as an argument"]'.
*/
declare const n: number;
foo(n);
declare const n2: 42 | 69 | 78;
//Long, ugly, error message
foo(n2);
///// Chaining calls/Generics
function bar<T extends number>(n: T) {
//NOT OK; Long, ugly, error message
return foo(n);
}
function baz<T extends number>(n: T & ErrorCheck<T>) {
//Still NOT OK; Long, ugly, error message
return foo(n);
}
function buzz<T extends number>(n: T & ErrorCheck<T>) {
//OK!
return foo<T>(n)
} |
desperately need a special type (let's call it function f<A>(something: A): string {
type A extends { text: string; } ? A : invalid; // might need to stop here if A turns out to be bad
return something.text; // if we got here then A is { text: string; }
} |
@Aleksey-Bykov Why don't you just do this? function f<A extends { text: string; }>(something: A): string {
return something.text;
} |
well because my main use case is inferring types from generics via |
+1 This would be great for creation of an XOR type, because the errors otherwise are unreadable unless you do the following. type Without<T, U> = {
[P in Exclude<keyof T, keyof U>]?: ["Property", P, "from type", T, "is mutually exclusive with the following properties in type", U, keyof U]
};
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
interface Y { y: number };
interface Z { z: string };
var x: XOR<Y, Z> = {
y: 2,
z: ""
} The following would be preferable. I'd just suggest sticking with interpolation syntax for consistency. type Without<T, U> = {
[P in Exclude<keyof T, keyof U>]?: throw `Property ${P} from type ${T} is mutually exclusive with the following properties in type ${U} : ${keyof U}`
}; If this syntax were accepted, a future addition could be a handle to the current instance of the object. throw `Property ${P} from type ${T} is mutually exclusive with the following properties ${keyof Extract<U, typeof this>}` This would restrict the error down to the properties actually used by the inline type. |
1000x yes to this. As a library author it's tempting to go wild with the expressivity of TS and end up producing some lovely useful safe abstractions... which give awful unhelpful type error messages when users make mistakes. So I will often trade off nice abstractions in favour of nice error messages. This feature would mean I could avoid making those tradeoffs. I could give users wild, beautiful, feels-good-man type abstractions at the same time as giving users clear, domain-specific error messages. |
Thanks a lot! |
I'm working on this with PR #40336, I think it will be super powerful. |
PR at #40402 |
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). |
It's possible that a small extension of the new type IsStringLiteral<T extends string> = string extends T ? never : T;
type Foo<T satisfies IsStringLiteral<T>> = ...; then you could write
Here's a more concrete motivation (playground): declare function isEnum<T extends IsEnum<T>>(enumContainer: T): (arg: unknown) => arg is T[keyof T];
type IsEnum<T, E = T[keyof T]> =
[E] extends [string] ? (string extends E ? never : unknown) :
[E] extends [number] ? (
true extends ({[key: number]: true} & {[P in E]: false})[number] ?
unknown : never) :
never; I want to lock down declare function isEnum<T satisfies IsEnum<T>>(enumContainer: T): (arg: unknown) => arg is T[keyof T]; then the type checker could still check the constraint but not establish a cycle. |
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. |
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. |
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 I wonder whether |
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. |
Proposal
A new
invalid
type that is not assignable to or from any other types. This includes not being assignable to or fromany
ornever
. It probably shouldn't even be assignable toinvalid
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 toany
andinvalid & never
is not reduced tonever
.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
orfalse
but notboolean
- #23493 (comment)It's possible to write the above example today(playground link) using
never
instead ofinvalid
, 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)
Today(playground link) using
never
instead ofinvalid
the error message forerror1
is: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: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 theinfer
capability. For example at the end of #21496 there is a type:Maybe
invalid<"should never happen">
is used instead ofnever
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 ifT
isany
, both the true and false branches are always taken so you might not want to change it away fromnever
, 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
The text was updated successfully, but these errors were encountered: