gvergnaud / ts-pattern

🎨 The exhaustive Pattern Matching library for TypeScript, with smart type inference.
MIT License
12.44k stars 131 forks source link

P.instanceof cannot be used for classes which has a private constructor. #285

Open LumaKernel opened 1 month ago

LumaKernel commented 1 month ago

Describe the bug A clear and concise description of what the bug is.

if (instanceof C) can be used for classes which has private constructors potentially, but P.instanceof can't. It's because type AnyConstructor = abstract new (...args: any[]) => any; cannot accept private constructors.

TypeScript playground with a minimal reproduction case

Playground

export class ColorRgb {
  private constructor(
    public readonly r: number,
    public readonly g: number,
    public readonly b: number,
  ) {
    if (new.target !== ColorRgb) {
      throw new Error('OptionRed cannot be extended');
    }
    if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
      throw new Error('Invalid color');
    }
  }
  static new(r: number, g: number, b: number): ColorRgb {
    return new ColorRgb(r, g, b);
  }
}
export class ColorHsl {
  private constructor(
    public readonly h: number,
    public readonly s: number,
    public readonly l: number,
  ) {
    if (new.target !== ColorHsl) {
      throw new Error('ColorHsl cannot be extended');
    }
    if (h < 0 || h > 360 || s < 0 || s > 100 || l < 0 || l > 100) {
      throw new Error('Invalid color');
    }
  }
  new(h: number, s: number, l: number): ColorHsl {
    return new ColorHsl(h, s, l);
  }
}

type Color = ColorRgb | ColorHsl;

const color: Color = ColorRgb.new(20, 40, 60);
if (color instanceof ColorRgb) {
  color;
  // ^?
}

// Type error!
match(color)
  .with(P.instanceOf(ColorRgb), (color) => {
    console.log(`rgb(${color.r}, ${color.g}, ${color.b})`);
  })
  .with(P.instanceOf(ColorHsl), (color) => {
    console.log(`hsl(${color.h}, ${color.s}, ${color.l})`);
  })
  .exhaustive();

Versions

I tried to find the universal class of all classes including private constructors, but I coundn't get it. Maybe I'm hitting the limitation of TypeScript...

LumaKernel commented 1 month ago

here, it's important to combine

LumaKernel commented 1 month ago

By trying {} instanceof {}, I got the following message.

The right-hand side of an 'instanceof' expression must be either of type
'any', a class, function, or other type assignable to the 'Function'
interface type, or an object type with a 'Symbol.hasInstance' method.

So it seems we can just use Function | { readonly [Symbol.hasInstance]: any } instead to support any types which can be also used in instanceof right-hand operand.

gvergnaud commented 1 month ago

Ah thanks for the report, that's too bad. I'm not sure I can fix it in ts-pattern because instanceOf requires it's parameter to be a subtype of abstract new (...any) => any in order to pass it to the native InstanceType helper. Without this contraint, I can't narrow the input type for this branch:

export class Color {
  private constructor(
    public readonly r: number,
    public readonly g: number,
    public readonly b: number,
  ) {}
  static new(r: number, g: number, b: number): Color {
    return new Color(r, g, b);
  }
}

type X = InstanceType<Color> // ❌

Playground