Skip to content

Commit 551d96f

Browse files
Class utility types for abstract or concrete classes
1 parent f3057f9 commit 551d96f

3 files changed

Lines changed: 217 additions & 0 deletions

File tree

class.test.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { assert, assertEquals } from '@std/assert';
2+
import type { Abstract, Concrete } from './class.ts';
3+
4+
type IsEqual<X, Y> = (
5+
<E>() => E extends X ? 1 : 2
6+
) extends (
7+
<E>() => E extends Y ? 1 : 2
8+
) ? true
9+
: false;
10+
11+
Deno.test('Concrete', () => {
12+
abstract class Base {
13+
public readonly a: number;
14+
protected readonly b: number;
15+
private readonly c: number;
16+
17+
constructor(a: number, b: number, c?: number) {
18+
this.a = a;
19+
this.b = b;
20+
this.c = c ?? 0;
21+
}
22+
23+
values(): [number, number, number] {
24+
return [this.a, this.b, this.c];
25+
}
26+
27+
public static readonly PUB = 1;
28+
protected static readonly PRO = 2;
29+
private static readonly PRI = 3;
30+
31+
static values(): [number, number, number] {
32+
return [this.PUB, this.PRO, this.PRI];
33+
}
34+
35+
static new(): string {
36+
return 'new';
37+
}
38+
}
39+
40+
class Impl extends Base {}
41+
42+
{
43+
const CB: Concrete<typeof Base> = Impl;
44+
45+
assert(true satisfies IsEqual<(typeof CB)['prototype'], Base>);
46+
assert(
47+
true satisfies IsEqual<
48+
ConstructorParameters<typeof CB>,
49+
[a: number, b: number, c?: number | undefined]
50+
>,
51+
);
52+
assert(true satisfies IsEqual<InstanceType<typeof CB>, Impl>);
53+
54+
assertEquals((new CB(1, 2)).values(), [1, 2, 0]);
55+
assertEquals((new CB(1, 2, 3)).values(), [1, 2, 3]);
56+
assertEquals(CB.values(), [1, 2, 3]);
57+
assertEquals(CB.new(), 'new');
58+
}
59+
60+
{
61+
const CI: Concrete<typeof Impl> = Impl;
62+
63+
assert(true satisfies IsEqual<(typeof CI)['prototype'], Impl>);
64+
assert(
65+
true satisfies IsEqual<
66+
ConstructorParameters<typeof CI>,
67+
[a: number, b: number, c?: number | undefined]
68+
>,
69+
);
70+
assert(true satisfies IsEqual<InstanceType<typeof CI>, Impl>);
71+
72+
assertEquals((new CI(1, 2)).values(), [1, 2, 0]);
73+
assertEquals((new CI(1, 2, 3)).values(), [1, 2, 3]);
74+
assertEquals(CI.values(), [1, 2, 3]);
75+
assertEquals(CI.new(), 'new');
76+
}
77+
78+
{
79+
// @ts-expect-error Class only.
80+
// deno-lint-ignore ban-types
81+
type Type = Concrete<Function>;
82+
assert(true satisfies IsEqual<Type, never>);
83+
}
84+
85+
{
86+
// @ts-expect-error Class only.
87+
type Type = Concrete<() => void>;
88+
assert(true satisfies IsEqual<Type, never>);
89+
}
90+
});
91+
92+
Deno.test('Abstract', () => {
93+
class Real {
94+
public readonly a: number;
95+
protected readonly b: number;
96+
private readonly c: number;
97+
98+
constructor(a: number, b: number, c?: number) {
99+
this.a = a;
100+
this.b = b;
101+
this.c = c ?? 0;
102+
}
103+
104+
values(): [number, number, number] {
105+
return [this.a, this.b, this.c];
106+
}
107+
108+
public static readonly PUB = 1;
109+
protected static readonly PRO = 2;
110+
private static readonly PRI = 3;
111+
112+
static values(): [number, number, number] {
113+
return [this.PUB, this.PRO, this.PRI];
114+
}
115+
116+
static new(): string {
117+
return 'new';
118+
}
119+
}
120+
121+
const Base: Abstract<typeof Real> = Real;
122+
123+
class Impl extends Base {}
124+
125+
{
126+
const CB: Concrete<typeof Base> = Impl;
127+
128+
assert(true satisfies IsEqual<(typeof CB)['prototype'], Real>);
129+
assert(
130+
true satisfies IsEqual<
131+
ConstructorParameters<typeof CB>,
132+
[a: number, b: number, c?: number | undefined]
133+
>,
134+
);
135+
assert(true satisfies IsEqual<InstanceType<typeof CB>, Impl>);
136+
137+
assertEquals((new CB(1, 2)).values(), [1, 2, 0]);
138+
assertEquals((new CB(1, 2, 3)).values(), [1, 2, 3]);
139+
assertEquals(CB.values(), [1, 2, 3]);
140+
assertEquals(CB.new(), 'new');
141+
}
142+
143+
{
144+
const CI: Concrete<typeof Impl> = Impl;
145+
146+
assert(true satisfies IsEqual<(typeof CI)['prototype'], Impl>);
147+
assert(
148+
true satisfies IsEqual<
149+
ConstructorParameters<typeof CI>,
150+
[a: number, b: number, c?: number | undefined]
151+
>,
152+
);
153+
assert(true satisfies IsEqual<InstanceType<typeof CI>, Impl>);
154+
155+
assertEquals((new CI(1, 2)).values(), [1, 2, 0]);
156+
assertEquals((new CI(1, 2, 3)).values(), [1, 2, 3]);
157+
assertEquals(CI.values(), [1, 2, 3]);
158+
assertEquals(CI.new(), 'new');
159+
}
160+
161+
{
162+
// @ts-expect-error Class only.
163+
// deno-lint-ignore ban-types
164+
type Type = Abstract<Function>;
165+
assert(true satisfies IsEqual<Type, never>);
166+
}
167+
168+
{
169+
// @ts-expect-error Class only.
170+
type Type = Abstract<() => void>;
171+
assert(true satisfies IsEqual<Type, never>);
172+
}
173+
174+
{
175+
// @ts-expect-error Abstract.
176+
const o = new Base();
177+
assert(o);
178+
}
179+
180+
class Bad extends Base {
181+
constructor() {
182+
// @ts-expect-error Abstract.
183+
super();
184+
}
185+
}
186+
assert(Bad);
187+
});

class.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @module
3+
*
4+
* Class utilities.
5+
*/
6+
7+
// deno-lint-ignore-file no-explicit-any
8+
9+
/**
10+
* Convert a class to an abstract class.
11+
*
12+
* @template C Class.
13+
*/
14+
export type Abstract<
15+
C extends abstract new (...args: any[]) => any,
16+
> = C extends abstract new (...args: infer A) => infer R
17+
? (abstract new (...args: A) => R) & C
18+
: never;
19+
20+
/**
21+
* Convert a class to a concrete class.
22+
*
23+
* @template C Class.
24+
*/
25+
export type Concrete<
26+
C extends abstract new (...args: any[]) => any,
27+
> = C extends abstract new (...args: infer A) => infer R
28+
? (new (...args: A) => R) & C
29+
: never;

mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
* Everything class.
55
*/
66

7+
export * from './class.ts';
78
export * from './constant.ts';
89
export * from './readonly.ts';

0 commit comments

Comments
 (0)