Skip to content

Commit f6a448c

Browse files
Added generic class type
1 parent b594dc4 commit f6a448c

2 files changed

Lines changed: 175 additions & 3 deletions

File tree

class.test.ts

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { assert, assertEquals } from '@std/assert';
2-
import type { Abstract, Concrete } from './class.ts';
1+
import { assert, assertEquals, assertInstanceOf } from '@std/assert';
2+
import type { Abstract, Class, Concrete } from './class.ts';
33

44
type IsEqual<X, Y> = (
55
<E>() => E extends X ? 1 : 2
@@ -204,3 +204,164 @@ Deno.test('Abstract: super', () => {
204204
}
205205
assert(BadSuper);
206206
});
207+
208+
Deno.test('Class: only', () => {
209+
function getToString(c: Class): unknown {
210+
return c.toString;
211+
}
212+
213+
assertEquals(typeof getToString(BaseC), 'function');
214+
assertEquals(typeof getToString(Object), 'function');
215+
assertEquals(typeof getToString(Function), 'function');
216+
assertEquals(typeof getToString(class {}), 'function');
217+
218+
// @ts-expect-error Not a class.
219+
const badFunc = getToString(function (): number {
220+
return 1;
221+
});
222+
assertEquals(typeof badFunc, 'function');
223+
224+
// @ts-expect-error Not a class.
225+
const badArrow = getToString(() => 1);
226+
assertEquals(typeof badArrow, 'function');
227+
228+
// @ts-expect-error Not a class.
229+
const badObj = getToString({});
230+
assertEquals(typeof badObj, 'function');
231+
});
232+
233+
Deno.test('Class: members', () => {
234+
/**
235+
* Get member of class.
236+
*
237+
* @param c Class.
238+
* @returns Member.
239+
*/
240+
function getPUB(c: Class<{ PUB: number }>): unknown {
241+
return c.PUB;
242+
}
243+
244+
assertEquals(getPUB(BaseA), 1);
245+
assertEquals(getPUB(BaseC), 1);
246+
247+
class Bad {}
248+
249+
// @ts-expect-error Missing.
250+
const bad = getPUB(Bad);
251+
assertEquals(bad, undefined);
252+
});
253+
254+
Deno.test('Class: constructor: concrete', () => {
255+
class A {
256+
declare public readonly ['constructor']: Class<typeof A>;
257+
public a = 1;
258+
public readonly value: string;
259+
constructor(value: string) {
260+
this.value = value;
261+
}
262+
public static A = 1;
263+
public static new(): string {
264+
return 'new';
265+
}
266+
}
267+
268+
class B extends A {
269+
declare public readonly ['constructor']: Class<typeof B>;
270+
public b = 2;
271+
constructor(value: number) {
272+
super(String(value));
273+
}
274+
public static B = 2;
275+
}
276+
277+
class C extends B {
278+
declare public readonly ['constructor']: typeof C;
279+
public c = 3;
280+
constructor(value: boolean) {
281+
super(value ? 1 : 0);
282+
}
283+
public static C = 3;
284+
}
285+
286+
const a: A = new A('a');
287+
// @ts-expect-error Abstract.
288+
const a2: A = new a.constructor('a');
289+
assertInstanceOf(a, A);
290+
assertInstanceOf(a2, A);
291+
assertEquals(a.constructor.A, 1);
292+
assertEquals(a.constructor.new(), 'new');
293+
294+
const b: B = new B(2);
295+
// @ts-expect-error Abstract.
296+
const b2: B = new b.constructor(2);
297+
assertInstanceOf(b, B);
298+
assertInstanceOf(b2, B);
299+
assertEquals(b.constructor.B, 2);
300+
assertEquals(b.constructor.new(), 'new');
301+
302+
const c: C = new C(true);
303+
const c2: C = new c.constructor(true);
304+
assertInstanceOf(c, C);
305+
assertInstanceOf(c2, C);
306+
assertEquals(c.constructor.C, 3);
307+
assertEquals(c.constructor.new(), 'new');
308+
});
309+
310+
Deno.test('Class: constructor: abstract', () => {
311+
abstract class A {
312+
declare public readonly ['constructor']: Class<typeof A>;
313+
public a = 1;
314+
public readonly value: string;
315+
constructor(value: string) {
316+
this.value = value;
317+
}
318+
public static A = 1;
319+
public static new(): string {
320+
return 'new';
321+
}
322+
}
323+
324+
class B extends A {
325+
declare public readonly ['constructor']: Class<typeof B>;
326+
public b = 2;
327+
constructor(value: number) {
328+
super(String(value));
329+
}
330+
public static B = 2;
331+
}
332+
333+
abstract class C extends B {
334+
declare public readonly ['constructor']: typeof C;
335+
public c = 3;
336+
constructor(value: boolean) {
337+
super(value ? 1 : 0);
338+
}
339+
public static C = 3;
340+
}
341+
342+
// @ts-expect-error Abstract.
343+
const a: A = new A('a');
344+
// @ts-expect-error Abstract.
345+
const a2: A = new a.constructor('a');
346+
assertInstanceOf(a, A);
347+
assertInstanceOf(a2, A);
348+
assertEquals(a.constructor.A, 1);
349+
assertEquals(a.constructor.new(), 'new');
350+
351+
const b: B = new B(2);
352+
// @ts-expect-error Abstract.
353+
const b2: B = new b.constructor(2);
354+
assertInstanceOf(b, B);
355+
assertInstanceOf(b2, B);
356+
assertEquals(b.constructor.B, 2);
357+
assertEquals(b.constructor.new(), 'new');
358+
359+
// @ts-expect-error Abstract.
360+
const c: C = new C(true);
361+
// @ts-expect-error Abstract.
362+
const c2: C = new c.constructor(true);
363+
assertInstanceOf(c, C);
364+
assertInstanceOf(c2, C);
365+
assertEquals(c.constructor.C, 3);
366+
assertEquals(c.constructor.new(), 'new');
367+
});

class.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Class utilities.
55
*/
66

7-
// deno-lint-ignore-file no-explicit-any
7+
// deno-lint-ignore-file no-explicit-any ban-types
88

99
/**
1010
* Convert a class to an abstract class.
@@ -29,3 +29,14 @@ export type Concrete<
2929
> = C extends abstract new (...args: infer A) => infer R
3030
? (new (...args: A) => R) & Omit<C, never>
3131
: never;
32+
33+
/**
34+
* Generic class with optional static properties.
35+
*
36+
* @template C Static properties.
37+
* @returns Class type.
38+
*/
39+
export type Class<C extends object = {}> =
40+
& (abstract new (...args: any[]) => any)
41+
& { readonly prototype: Object | null }
42+
& Omit<C, 'prototype'>;

0 commit comments

Comments
 (0)