question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

new.target type is too narrow

See original GitHub issue

Bug Report

🔎 Search Terms

new.target, narrow

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about new.target

⏯ Playground Link

Playground link with relevant code

💻 Code

class AssertionError extends Error {
    name = "AssertionError";
}

function assert(
    boolean: boolean,
    message: string = "Assertion failed",
): asserts boolean {
    if (!boolean) {
        throw new AssertionError(message);
    }
}

type PathInit = {
    isRooted: boolean,
    parts: ReadonlyArray<string>,
};

type AbsolutePathLike = AbsolutePath | `/${ string }`;
type PathLike = Path | string;

abstract class Path {
    static parse(path: `/${ string }`): AbsolutePath;
    static parse(path: string): AbsolutePath | RelativePath;
    static parse(path: string): AbsolutePath | RelativePath {
        const isRooted = path.startsWith("/");
        if (isRooted) {
            return new AbsolutePath(path.slice(1).split("/"));
        }
        return new RelativePath(path.split("/"));
    }
    static from(path: AbsolutePathLike): AbsolutePath;
    static from(path: RelativePath): RelativePath;
    static from(path: PathLike): AbsolutePath | RelativePath;
    static from(path: PathLike): AbsolutePath | RelativePath {
        if (typeof path === "string") {
            return Path.parse(path);
        }
        return path;
    }
    static join(
        first: RelativePath,
        ...paths: Array<PathLike>
    ): RelativePath;
    static join(
        first: AbsolutePathLike,
        ...paths: Array<PathLike>
    ): AbsolutePath;
    static join(
        first: PathLike,
        ...paths: Array<PathLike>
    ): AbsolutePath | RelativePath;
    static join(
        first: PathLike,
        ...paths: Array<PathLike>
    ): AbsolutePath | RelativePath {
        first = Path.from(first);
        const allPaths = [first, ...paths].map((path) => Path.from(path));
        if (first.isAbsolute()) {
            return new AbsolutePath(
                allPaths.flatMap((path) => path.#parts),
            );
        }
        assert(first.isRelative());
        return new RelativePath(
            allPaths.flatMap((path) => path.#parts),
        );
    }
    readonly #isRooted: boolean;
    readonly #parts: ReadonlyArray<string>;
    constructor({
        isRooted,
        parts,
    }: PathInit) {
        if (new.target !== AbsolutePath
        && new.target !== RelativePath) {
            throw new TypeError("Path should not be constructed directly");
        }
        const realParts: Array<string> = [];
        for (const part of parts) {
            if (part.includes("/")) {
                throw new RangeError(`path segments may not contain "/" characters`);
            } else if (part === "." || part === "") {
                continue;
            } else {
                realParts.push(part);
            }
        }
        this.#isRooted = isRooted;
        this.#parts = Object.freeze(realParts);
    }
    isAbsolute(): this is AbsolutePath {
        return this.#isRooted;
    }
    isRelative(): this is RelativePath {
        return !this.#isRooted;
    }
    #toString(): string {
        let path = this.#parts.join("/");
        if (this.#isRooted) {
            path = `/${ path }`;
        }
        return path;
    }
    get path(): string {
        return this.#toString();
    }
    get parts(): ReadonlyArray<string> {
        return this.#parts;
    }
}
class RelativePath extends Path {
    constructor(parts: ReadonlyArray<string>) {
        super({
            isRooted: false,
            parts,
        });
    }
}
class AbsolutePath extends Path {
    constructor(parts: ReadonlyArray<string>) {
        super({
            isRooted: true,
            parts,
        });
    }
    get fileURL(): string {
        const url = new URL("file://");
        url.pathname = `/${ this.parts.map((part) => encodeURIComponent(part)).join("/") }`;
        return url.href;
    }
}

🙁 Actual behavior

The value of new.target is too narrow as it does not actually allow proper subclasses to assume the value of new.target despite this being entirely possible. This is because, in this case, typeof Path has an incompatible construct signature with typeof AbsolutePath/RelativePath.

🙂 Expected behavior

In general subclasses can be expected to change the signature of the constructor, this is a core part of class-based OO, hence new.target type is overspecified.

I think the simplest resolution would be to simply widen new.target to new (...args: any[]) => this, as the only thing guaranteed about a subclass signature is that it will return an instance of this.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:10 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
Jamesernatorcommented, Mar 21, 2022

Smaller example that’s not as motivating, but it demonstrates the issue in a minimal way:

class Class {
    constructor(x: number) {
        // This condition will always return 'false' since the types 'typeof Class' and 'typeof Subclass' have no overlap.
        if (new.target === Subclass) {

        }
    }
}

class Subclass extends Class {
    constructor(arg: [number, number]) {
        super(arg[0]);
    }
}
0reactions
DanielRosenwassercommented, Mar 22, 2022

We could maybe imagine something like that. Another approach is to say that instead of checking for a some form of compatibility from each type (a relationship check that we call comparability today), we could try another approach where we just check whether the intersection of both sides of a type form a non-empty type. I think @ahejlsberg and @weswigham have brought up in the past.

The problem with that is that almost every single object type ruins this. An object type forms a non-empty intersection with almost everything.

To start, here’s a questionable example:

interface Equalalable {
    equals(other: unknown): boolean;
}

interface Barkable {
    bark(): void;
}

declare const dog: Barkable;
declare const horse: Equalalable;

function foo(dog: Barkable, horse: Equalalable) {
    // Not allowed today, would be allowed under intersection rules.
    if (dog === horse) {
        // ...
    }
}

This produces runnable JavaScript, but questionable. The object hierarchies theoretically overlap, but this has the chance to catch logic errors today across unrelated types.

Here’s a severely questionable example.

interface TrafficLight {
    state: "red" | "green" | "yellow"
}

function drive(trafficLight: TrafficLight) {
    // Not allowed today, would be allowed under intersection rules.
    if (trafficLight !== "red") {
        // ...
    }
}

This also produces runnable JavaScript, but the if block will always run. That’s because the intersection of "red" and TrafficLight is considered non-vacuous. We assume "red" & TrafficLight is a type that can exist (and while it technically can, we all know that’s bogus for all practical purposes).

So I think intersection takes us to the other extreme of having no guarantees in === checks.

Maybe we could

  • vary this behavior on a strict-mode flag? (seems questionable)
  • special-case how literals form intersections for this operation?

Both seem like areas we could experiment in.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Documentation - Narrowing - TypeScript
JavaScript has an operator for determining if an object has a property with a name: the in operator. TypeScript takes this into account...
Read more >
How to display shortcuts target details in Windows Explorer
In the pop-up window, tick "Link Target". You will then be able to see the targets of shortcuts in that folder.
Read more >
Exclusive: Walmart's new redesign looks a lot like Target
Walmart's redesign channels a classic department store in the same way Target's does. But it's less concerned about fit and finish.
Read more >
Revised RECIST guideline (version 1.1)
New response evaluation criteria in solid tumours: Revised RECIST guideline (version 1.1) ... Target lesions that become 'too small to measure'. While on....
Read more >
What Is a Target Market (And How to Find Yours in 2023)
But vegetarians are a surprisingly small target market segment for Impossible ... But knowing which decade of life your customers are in can...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found