Skip to content

Commit

Permalink
Fix accessing prototype property when using Class and `AbstractCl…
Browse files Browse the repository at this point in the history
…ass` types (#632)
  • Loading branch information
ajvincent committed Jun 20, 2023
1 parent 728626a commit 8edb681
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 18 deletions.
13 changes: 11 additions & 2 deletions source/basic.d.ts
Expand Up @@ -3,7 +3,10 @@ Matches a [`class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe
@category Class
*/
export type Class<T, Arguments extends unknown[] = any[]> = Constructor<T, Arguments> & {prototype: T};
export type Class<T, Arguments extends unknown[] = any[]> = {
prototype: T;
new(...arguments_: Arguments): T;
};

/**
Matches a [`class` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes).
Expand All @@ -16,8 +19,14 @@ export type Constructor<T, Arguments extends unknown[] = any[]> = new(...argumen
Matches an [`abstract class`](https://www.typescriptlang.org/docs/handbook/classes.html#abstract-classes).
@category Class
@privateRemarks
We cannot use a `type` here because TypeScript throws: 'abstract' modifier cannot appear on a type member. (1070)
*/
export type AbstractClass<T, Arguments extends unknown[] = any[]> = AbstractConstructor<T, Arguments> & {prototype: T};
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export interface AbstractClass<T, Arguments extends unknown[] = any[]> extends AbstractConstructor<T, Arguments> {
prototype: T;
}

/**
Matches an [`abstract class`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-2.html#abstract-construct-signatures) constructor.
Expand Down
49 changes: 35 additions & 14 deletions test-d/abstract-class.ts
@@ -1,15 +1,19 @@
import {expectError} from 'tsd';
import {expectError, expectAssignable, expectNotAssignable} from 'tsd';
import type {AbstractConstructor, AbstractClass} from '../index';

abstract class Foo {
constructor(x: number) {
void (x);
}

abstract fooMethod(): void;
}

abstract class Bar {
abstract barMethod(): void;
}

function functionRecevingAbsClass<T>(cls: AbstractClass<T>) {
function functionReceivingAbsClass<T>(cls: AbstractClass<T>) {
return cls;
}

Expand All @@ -21,20 +25,37 @@ function withBar<T extends AbstractConstructor<object>>(Ctor: T) {
return ExtendedBar;
}

function assertWithBar() {
// This lacks `barMethod`.
// @ts-expect-error
class WrongConcreteExtendedBar extends withBar(Bar) {}
// This lacks `barMethod`.
// @ts-expect-error
class WrongConcreteExtendedBar extends withBar(Bar) {}

// This should be alright since `barMethod` is implemented.
class CorrectConcreteExtendedBar extends withBar(Bar) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
barMethod() {}
// This should be alright since `barMethod` is implemented.
class CorrectConcreteExtendedBar extends withBar(Bar) {
constructor(x: number, y: number) {
super();
void (x);
void (y);
}
functionRecevingAbsClass<Bar>(withBar(Bar));
functionRecevingAbsClass<Bar>(CorrectConcreteExtendedBar);

// eslint-disable-next-line @typescript-eslint/no-empty-function
barMethod() {}
}

functionRecevingAbsClass(Foo);
expectError(functionRecevingAbsClass<Bar>(Foo));
function assertWithBar() {
functionReceivingAbsClass<Bar>(withBar(Bar));
functionReceivingAbsClass<Bar>(CorrectConcreteExtendedBar);
}

functionReceivingAbsClass(Foo);
expectError(functionReceivingAbsClass<Bar>(Foo));
assertWithBar();

expectAssignable<AbstractConstructor<{barMethod(): void}, []>>(Bar);
expectAssignable<AbstractClass<{barMethod(): void}, []>>(Bar);

// Prototype test
expectAssignable<{barMethod(): void}>(Bar.prototype);
expectNotAssignable<{fooMethod(): void}>(Bar.prototype);
expectError(new CorrectConcreteExtendedBar(12));
expectAssignable<{barMethod(): void}>(new CorrectConcreteExtendedBar(12, 15));
// /Prototype test
37 changes: 35 additions & 2 deletions test-d/class.ts
@@ -1,5 +1,5 @@
import {expectError} from 'tsd';
import type {Constructor} from '../index';
import {expectAssignable, expectError, expectNotAssignable, expectType} from 'tsd';
import type {Class, Constructor, IsAny} from '../index';

class Foo {
constructor(x: number, y: any) {
Expand All @@ -21,3 +21,36 @@ function fn2(Cls: Constructor<Foo, [number, number]>): Foo {

fn(Foo);
fn2(Foo);

// Prototype test
type PositionProps = {
top: number;
left: number;
};

class Position {
public top: number;

public left: number;

constructor(parameterTop: number, parameterLeft: number) {
this.top = parameterTop;
this.left = parameterLeft;
}
}

declare const Bar: Class<PositionProps>;

expectAssignable<Class<PositionProps>>(Position);

expectNotAssignable<Class<PositionProps, [number]>>(Position);

expectAssignable<Class<PositionProps, [number, number]>>(Position);
expectAssignable<Constructor<PositionProps, [number, number]>>(Position);

expectType<IsAny<typeof Bar['prototype']>>(false);
expectType<PositionProps>(Position.prototype);
// /Prototype test

expectError(new Position(17));
expectAssignable<PositionProps>(new Position(17, 34));

0 comments on commit 8edb681

Please sign in to comment.