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

Feature Request: Custom errors via type param utility when used as a default #41392

Closed
5 tasks done
harrysolovay opened this issue Nov 4, 2020 · 6 comments
Closed
5 tasks done
Labels
Duplicate An existing issue was already created

Comments

@harrysolovay
Copy link

harrysolovay commented Nov 4, 2020

Search Terms

Throw, error, in, type, param, custom, message, assert

The Problem

The ability to signal sources of error is critical to the productivity of both library developers and consumers; without this ability, it's easy to get lost in a sea of difficult-to-trace nevers.

PR #40468 is incredibly exciting. However, it's unclear how users could specify the positioning and bubbling of error messages. This issue details a different approach, one using a would-be intrinsic ParamError utility type, which would be used in lieu of type variable defaults.

Suggestion

type IsValid<T> = // ... the predicate type

type WithComplexConstraint<
  InQuestion, // <-- the type in need of validation
  DidThrow = ParamError<"That's invalid!", InQuestion, IsValid<InQuestion> ? false : true>
  //                    ^                  ^           ^
  //                    |                  |           |
  //                    |                  |           3. A boolean representing whether or not to error)
  //                    |                  2. The type param over which to display the error
  //                    1. The error message
> = // ...

In the use case below, I describe a generic IsEmailAddress predicate type, and an acceptsEmailAddress function, which constrains the first param according to the IsEmailAddress predicate. Here's how such validation would look using ParamError:

type IsEmailAddress<S extends string> = // ...

declare function acceptsEmailAddress<
  S extends string,
  IsInvalid = ParamError<"The supplied string type is not an email address type.", S, IsEmailAddress<S>>
>(emailAddress: S): void;

acceptsEmailAddress("rickross@smoothrapping.com");
acceptsEmailAddress("toppotdoughnuts.com");
//                   ~~~~~~~~~~~~~~~~~~~~
//                   ^
//                   type-error: "The supplied string type is not an email address type."

One could have multiple validations target the same type param:

type IsEmailAddress<S extends string> = // ...
type IsHotmailDotCom<S extends string> = S extends `${infer Local}@hotmail.com` ? true : false;

declare function acceptsEmailAddress<
  S extends string,
  IsInvalid = ParamError<"The supplied string type is not an email address type.", S, IsEmailAddress<S>>,
  IsHotmail = ParamError<"I respect it, but cummon... hotmail... in 2020? You still playing Neopets too?", S, IsHotmailDotCom<S>>
>(emailAddress: S): void;

acceptsEmailAddress("rickyrose@hotmail.com");
//                  ~~~~~~~~~~~~~~~~~~~~~~
//                  ^
//                  type-error: "The supplied string type is not an email address type."
//                  type-error: "I respect it, but cummon... hotmail... in 2020? You still playing Neopets too?"

Use Case

While the key use case is enabling library developers to create simpler error messaging experiences (and put aside the practice of never-hunting), you may have noticed another critical use case: constraining params by more than just subtype. Thanks to conditional and recursive types, we can traverse large structures and make judgements predicated upon more than could be represented with a subtype check.

Let's say we want to validate that a string literal is an email address. We begin by building the IsEmail predicate and its children.

type IsValidLocal<S> extends string = // ...
type IsValidDomain<S extends string> = // ...
type IsValidExtension<S extends string> = // ...

type AllTrue<T extends boolean[]> = // ...

type IsEmail<S extends string> = S extends `${infer Local}@${infer Domain}.${infer Extension}`
  ? AllTrue<[IsValidLocal<Local>, IsValidDomain<Domain>, IsValidExtension<Extension>]>
  : false;

This is not a type that we can represent today as a generic constraint. Aka, we are unable to do the following:

type EmailAddress = // ... somehow with the constraints defined above

declare function acceptsEmailAddress(emailAddress: EmailAddress): void;

While I hope that conditional assignability soon solves this problem, we're currently stuck. The only solutions seem to be:

  1. Leave the validation dangling beneath the function's usage (difficult to maintain / enforce).
declare function acceptsEmailAddress(emailAddress: string): void;

const emailAddress = "rick@ross.com";
acceptsEmailAddress(emailAddress);

type EmailAddress = typeof emailAddress;
type UsedCorrectly = IsEmailAddress<EmailAddress>;
assert<IsExact<UsedCorrectly, true>>(true); // using `conditional-type-checks`
  1. Produce an error-indicating return signature and check for that signature where acceptsEmailAddress's output is referenced (also difficult to maintain / enforce).
type Error<Message extends string> = {error: Message};

declare function acceptsEmailAddress<E extends string>(emailAddress: E): IsEmailAddress<E> extends true ? Error<"Invalid email address supplied"> : string;

const emailAddress = "rick@ross.com";
const result = acceptsEmailAddress(emailAddress);

declare function acceptsResult(result: string): void;

acceptsResult(result); // throws a type error if `emailAddress`'s type does not pass the `IsEmail` check

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@MartinJohns
Copy link
Contributor

Duplicate of #23689.

@harrysolovay
Copy link
Author

@MartinJohns while the intention is similar, the approaches are most certainly different.

This issue's proposed utility type includes type params for specifying error location and for explicitly telling whether to throw (encourages predicate reuse).

The supposed duplicate's proposed utility type:

type invalid<M extends string> = intrinsic;

This issue's proposed utility type:

type ParamError<Message extends string, Target, ShouldThrow extends boolean> = intrinsic;
//                                      ^       ^
//                                      |       Different
//                                      Different

The greatest difference however, is that this issue describes a specific context in which to use this type (similarly to ThisType): the default position of a type variable:

type X<
  T,
  TIsInvalid = ParamError<"Invalid message", T, IsValid<T>>
> = // ...

This is very different from the supposed duplicate's proposed place of use. That proposed usage occurs through a conditional type, which is to then be used as the right-hand of an intersection with a type variable's constraint).

I'm not the arbiter of truth or kindness in OS... but hand-waving an issue as a duplicate is not productive. I'd love feedback, positive or negative. As long as you come to it with an open mind. Thank you.

@MartinJohns
Copy link
Contributor

The feature you want is the exact same: A way to provide custom errors. Only the approach and the details differ slightly. This could have been a comment to aid the discussion regarding the desired feature in a centralized place, instead of scattering it across issues.

This is not "hand-waving" or "closed mindness". It's simply pointing out that this feature was already requested before. :-)

@harrysolovay
Copy link
Author

the approach and the details differ slightly

A) "Slightly" is an understatement.
B) Approach and detail are meaningful.
C) This issue does not detract from issues with common goals... quite the opposite: it keeps them tidy and focused.

@MartinJohns, I'd be curious to hear your thoughts on this design idea. Not sure if you often give feedback, or if your goals are more in line with helping keep the peace / reduce noise in TS issues. Either way, gotta love this community ;)

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Nov 12, 2020
@RyanCavanaugh
Copy link
Member

I agree this is a duplicate of #23689 - we prefer to organize issues by use case / end-goal, not have competing separate issues for different syntactic representations.

@harrysolovay
Copy link
Author

Well alright. The oracle has spoken.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

3 participants