Skip to content

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

Closed

Description

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.

Activity

  1. MartinJohns commented on Nov 4, 2020

    @MartinJohns
    Contributor

    Duplicate of #23689.

  2. harrysolovay commented on Nov 4, 2020

    @harrysolovay
    Author

    Martin Johns (@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.

  3. MartinJohns commented on Nov 4, 2020

    @MartinJohns
    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. :-)

  4. harrysolovay commented on Nov 4, 2020

    @harrysolovay
    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.

    Martin Johns (@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 ;)

  5. RyanCavanaugh commented on Nov 12, 2020

    @RyanCavanaugh
    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.

  6. harrysolovay commented on Nov 12, 2020

    @harrysolovay
    Author

    Well alright. The oracle has spoken.

  7. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions