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

fix(cli/dts): Type Deno.errors.* as subclasses of Error #10702

Merged
merged 4 commits into from
Jul 26, 2021

Conversation

Liamolucko
Copy link
Contributor

Fixes #10640

@bartlomieju bartlomieju requested a review from kitsonk May 19, 2021 13:08
@bartlomieju
Copy link
Member

We need to be very careful if we want to land this PR, it must be backwards compatible change

Copy link
Contributor

@kitsonk kitsonk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. I think we need to go a slightly different route to ensure we don't have any unintentional breakages in existing code, so we need to effectively "narrow" the existing types instead of introducing new classes. I would prefer if we did something like this:

declare namespace Deno {
  interface DenoError<N extends string> extends Error {
    name: N;
  }

  interface DenoErrorConstructor<N extends string> extends ErrorConstructor {
    new (message?: string): DenoError<N>;
    (message?: string): DenoError<N>;
    readonly prototype: DenoError<N>;
  }

  /** A set of error constructors that are raised by Deno APIs. */
  export const errors: {
    AddrInUse: DenoErrorConstructor<"AddrInUse">;
    AddrNotAvailable: DenoErrorConstructor<"AddrNotAvailable">;
    AlreadyExists: DenoErrorConstructor<"AlreadyExists">;
    BadResource: DenoErrorConstructor<"BadResource">;
    Busy: DenoErrorConstructor<"Busy">;
    BrokenPipe: DenoErrorConstructor<"BrokenPipe">;
    ConnectionAborted: DenoErrorConstructor<"ConnectionAborted">;
    ConnectionRefused: DenoErrorConstructor<"ConnectionRefused">;
    ConnectionReset: DenoErrorConstructor<"ConnectionReset">;
    Http: DenoErrorConstructor<"Http">;
    Interrupted: DenoErrorConstructor<"Interrupted">;
    InvalidData: DenoErrorConstructor<"InvalidData">;
    NotConnected: DenoErrorConstructor<"NotConnected">;
    NotFound: DenoErrorConstructor<"NotFound">;
    NotSupported: DenoErrorConstructor<"NotSupported">;
    PermissionDenied: DenoErrorConstructor<"PermissionDenied">;
    TimedOut: DenoErrorConstructor<"TimedOut">;
    UnexpectedEof: DenoErrorConstructor<"UnexpectedEof">;
    WriteZero: DenoErrorConstructor<"WriteZero">;
  };
}

We should also provide a unit tests to validate the types are working as expected. The tests in cli/tests/unit/ will be type checked as part of the build, and so while the code can be non functional it should at least type check. Maybe something like:

unitTest(function errorTypeChecking() {
  const err: unknown = new Deno.errors.NotFound();
  if (err instanceof Deno.errors.NotFound) {
    assertEquals(err.name, "NotFound");
  }
}

If the type narrowing doesn't work, the property .name won't be on err.

@sant123
Copy link

sant123 commented Jun 1, 2021

@Liamolucko Do you think that is ok? 😅

@Liamolucko
Copy link
Contributor Author

I would prefer if we did something like this:

declare namespace Deno {
  interface DenoError<N extends string> extends Error {
    name: N;
  }

  interface DenoErrorConstructor<N extends string> extends ErrorConstructor {
    new (message?: string): DenoError<N>;
    (message?: string): DenoError<N>;
    readonly prototype: DenoError<N>;
  }

  /** A set of error constructors that are raised by Deno APIs. */
  export const errors: {
    AddrInUse: DenoErrorConstructor<"AddrInUse">;
    AddrNotAvailable: DenoErrorConstructor<"AddrNotAvailable">;
    AlreadyExists: DenoErrorConstructor<"AlreadyExists">;
    BadResource: DenoErrorConstructor<"BadResource">;
    Busy: DenoErrorConstructor<"Busy">;
    BrokenPipe: DenoErrorConstructor<"BrokenPipe">;
    ConnectionAborted: DenoErrorConstructor<"ConnectionAborted">;
    ConnectionRefused: DenoErrorConstructor<"ConnectionRefused">;
    ConnectionReset: DenoErrorConstructor<"ConnectionReset">;
    Http: DenoErrorConstructor<"Http">;
    Interrupted: DenoErrorConstructor<"Interrupted">;
    InvalidData: DenoErrorConstructor<"InvalidData">;
    NotConnected: DenoErrorConstructor<"NotConnected">;
    NotFound: DenoErrorConstructor<"NotFound">;
    NotSupported: DenoErrorConstructor<"NotSupported">;
    PermissionDenied: DenoErrorConstructor<"PermissionDenied">;
    TimedOut: DenoErrorConstructor<"TimedOut">;
    UnexpectedEof: DenoErrorConstructor<"UnexpectedEof">;
    WriteZero: DenoErrorConstructor<"WriteZero">;
  };
}

That fixes the issue with Error, but Deno.errors.* are still instanceof each other:

let err = new Deno.errors.AddrInUse();
if (err instanceof Deno.errors.AddrNotAvailable) {
    // Property 'message' does not exist on type 'never'.
    //   The intersection 'DenoError<"AddrInUse"> & DenoError<"AddrNotAvailable">' was reduced to 'never' because property 'name' has conflicting types in some constituents.
    //
    // I think TypeScript initially thinks the `instanceof` is always true but then gets confused because the types are incompatible.
    err.message
} else {
    // Property 'message' does not exist on type 'never'.
    err.message
}

I think TypeScript considers generics to never exist at runtime, so all versions of DenoError are considered instances of the same class.

Also, Deno.errors.* aren't currently callable as functions so that would have to be added for ErrorConstructor to be correct.

@sant123
Copy link

sant123 commented Jun 29, 2021

What do you think @kitsonk? 😁

@bartlomieju bartlomieju requested a review from kitsonk June 30, 2021 01:54
Copy link
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this change is backwards compatible I would love to get this into 1.12

@bartlomieju bartlomieju added this to the 1.12.0 milestone Jul 6, 2021
@bartlomieju bartlomieju modified the milestones: 1.12.0, 1.13.0 Jul 13, 2021
@bartlomieju
Copy link
Member

bartlomieju commented Jul 26, 2021

I verified that this PR causes no problems when running tests for deno_std (0.103.0)

@sant123
Copy link

sant123 commented Jul 26, 2021

So do you think this can be merged?

Copy link
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you Liam and sorry it took so long to merge it.

@bartlomieju bartlomieju merged commit 091a261 into denoland:main Jul 26, 2021
@Liamolucko Liamolucko deleted the error_subclass branch July 27, 2021 01:17
@lucacasonato
Copy link
Member

These changes broke old std versions: https://github.com/lucacasonato/esbuild_deno_loader/runs/3201002174

@sant123
Copy link

sant123 commented Jul 30, 2021

It breaks until [email protected] according with denoland/deno_std#831

name: "WriteZero";
}
export class UnexpectedEof extends Error {
name: "UnexpectedEof";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name: "UnexpectedEof" | "PartialReadError";

This fixes the error but may introduce future issues with name typing. Since is expected to have just one value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not really that important for name to be statically typed, so the simplest way to deal with it would probably just be to change it back to string.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or to not support versions below [email protected] 🤭 but got your point, thank you!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Deno.errors returns never if used with instanceof
5 participants