Skip to content

Commit c00e628

Browse files
authored
Merge pull request #28 from scramjetorg/task/generic-this
Polymorphic/generic this as a return type for subclasses
2 parents 003f8a2 + e05c530 commit c00e628

File tree

5 files changed

+268
-4
lines changed

5 files changed

+268
-4
lines changed

samples/generic-this-1.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// This is a sample file demonstarting the approached used for "polymorphic this"
2+
// for generic method inheritance. It can be run with:
3+
//
4+
// npm run build && node build/samples/generic-this-1.js"
5+
6+
class BaseClass<T> {
7+
public value: T;
8+
9+
constructor(value: T) {
10+
this.value = value;
11+
}
12+
13+
create(value: T): BaseClass<T>;
14+
create<U>(value: U): BaseClass<U>;
15+
create<U extends any>(value: U): BaseClass<U> {
16+
return new BaseClass<U>(value);
17+
}
18+
19+
method1(): this {
20+
return this;
21+
}
22+
23+
method2(): BaseClass<T> {
24+
return this;
25+
}
26+
27+
method3(value: T): BaseClass<T> {
28+
return this.create<T>(value);
29+
}
30+
31+
method5(value: T): BaseClass<T>;
32+
method5<U>(value: U): BaseClass<U>;
33+
method5<U extends any>(value: U): BaseClass<U> {
34+
return this.create<U>(value);
35+
}
36+
}
37+
38+
class DerivedClass<T> extends BaseClass<T> {
39+
create(value: T): DerivedClass<T>;
40+
create<U>(value: U): DerivedClass<U>;
41+
create<U extends any>(value: U): DerivedClass<U> {
42+
return new DerivedClass<U>(value);
43+
}
44+
45+
method2(): DerivedClass<T> {
46+
return super.method2() as DerivedClass<T>;
47+
}
48+
49+
method3(value: T): DerivedClass<T> {
50+
return super.method3(value) as DerivedClass<T>;
51+
}
52+
53+
method5(value: T): DerivedClass<T>;
54+
method5<U>(value: U): DerivedClass<U>;
55+
method5<U>(value: U): DerivedClass<U> {
56+
return super.method5(value) as DerivedClass<U>;
57+
}
58+
59+
ownMethod1() {
60+
console.log(this);
61+
}
62+
}
63+
64+
class DerivedClassFixedType extends DerivedClass<number> {
65+
create(value: number): DerivedClassFixedType {
66+
return new DerivedClassFixedType(value);
67+
}
68+
69+
method2(): DerivedClassFixedType {
70+
return super.method2() as DerivedClassFixedType;
71+
}
72+
73+
method3(value: number): DerivedClassFixedType {
74+
return super.method3(value) as DerivedClassFixedType;
75+
}
76+
77+
method5(value: number): DerivedClassFixedType {
78+
return super.method5(value) as DerivedClassFixedType;
79+
}
80+
81+
ownMethod2() {
82+
console.log(this);
83+
}
84+
}
85+
86+
class BaseClassFixedType extends BaseClass<string> {
87+
create(value: string): BaseClassFixedType {
88+
return new BaseClassFixedType(value);
89+
}
90+
91+
method2(): BaseClassFixedType {
92+
return super.method2() as BaseClassFixedType;
93+
}
94+
95+
method3(value: string): BaseClassFixedType {
96+
return super.method3(value) as BaseClassFixedType;
97+
}
98+
99+
method5(value: string): BaseClassFixedType {
100+
return super.method5(value) as BaseClassFixedType;
101+
}
102+
103+
ownMethod3() {
104+
console.log(this);
105+
}
106+
}
107+
108+
// --- BaseClass
109+
110+
const bcString = new BaseClass("foo");
111+
const bcString1 = bcString.method1();
112+
const bcString2 = bcString.method2();
113+
const bcString3 = bcString.method3("bar");
114+
const bcString5 = bcString.method5(123);
115+
116+
for (const instance of [bcString, bcString1, bcString2, bcString3, bcString5]) {
117+
console.log(`instanceof: ${ instance instanceof BaseClass }; constructor.name: ${ instance.constructor.name }`);
118+
}
119+
120+
// --- DerivedClass
121+
122+
const dcString = new DerivedClass("foo");
123+
const dcString1 = dcString.method1();
124+
const dcString2 = dcString.method2();
125+
const dcString3 = dcString.method3("bar");
126+
const dcString5 = dcString.method5(123);
127+
128+
for (const instance of [dcString, dcString1, dcString2, dcString3, dcString5]) {
129+
console.log(`instanceof: ${ instance instanceof DerivedClass }; constructor.name: ${ instance.constructor.name }`);
130+
}
131+
132+
dcString.ownMethod1();
133+
dcString1.ownMethod1();
134+
dcString2.ownMethod1();
135+
dcString3.ownMethod1();
136+
dcString5.ownMethod1();
137+
138+
// --- DerivedClassFixedType
139+
140+
const dcftString = new DerivedClassFixedType(123);
141+
const dcftString1 = dcftString.method1();
142+
const dcftString2 = dcftString.method2();
143+
const dcftString3 = dcftString.method3(456);
144+
const dcftString5 = dcftString.method5(123);
145+
146+
for (const instance of [dcftString, dcftString1, dcftString2, dcftString3, dcftString5]) {
147+
console.log(`instanceof: ${ instance instanceof DerivedClassFixedType }; constructor.name: ${ instance.constructor.name }`);
148+
}
149+
150+
dcftString.ownMethod2();
151+
dcftString1.ownMethod2();
152+
dcftString2.ownMethod2();
153+
dcftString3.ownMethod2();
154+
dcftString5.ownMethod2();
155+
156+
// --- BaseClassFixedType
157+
158+
const bcftString = new BaseClassFixedType("foo");
159+
const bcftString1 = bcftString.method1();
160+
const bcftString2 = bcftString.method2();
161+
const bcftString3 = bcftString.method3("bar");
162+
const bcftString5 = bcftString.method5("baz");
163+
// const toBcStrign6 = bcftString.method5(123); // This won't work for now
164+
165+
for (const instance of [bcftString, bcftString1, bcftString2, bcftString3, bcftString5]) {
166+
console.log(`instanceof: ${ instance instanceof BaseClassFixedType }; constructor.name: ${ instance.constructor.name }`);
167+
}
168+
169+
bcftString.ownMethod3();
170+
bcftString1.ownMethod3();
171+
bcftString2.ownMethod3();
172+
bcftString3.ownMethod3();
173+
bcftString5.ownMethod3();

src/streams/data-stream.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ export class DataStream<T> implements BaseStream<T>, AsyncIterable<T> {
5050
};
5151
}
5252

53+
create(): DataStream<T>;
54+
create<U>(): DataStream<U>;
55+
create<U>(): DataStream<U> {
56+
return new DataStream<U>();
57+
}
58+
5359
map<U, W extends any[] = []>(callback: TransformFunction<T, U, W>, ...args: W): DataStream<U> {
5460
if (args?.length) {
5561
this.ifca.addTransform(this.injectArgsToCallback<U, typeof args>(callback, args));
@@ -70,6 +76,8 @@ export class DataStream<T> implements BaseStream<T>, AsyncIterable<T> {
7076
return this;
7177
}
7278

79+
flatMap<W extends any[] = []>(callback: TransformFunction<T, AnyIterable<T>, W>, ...args: W): DataStream<T>;
80+
flatMap<U, W extends any[] = []>(callback: TransformFunction<T, AnyIterable<U>, W>, ...args: W): DataStream<U>
7381
flatMap<U, W extends any[] = []>(callback: TransformFunction<T, AnyIterable<U>, W>, ...args: W): DataStream<U> {
7482
return this.asNewFlattenedStream(this.map<AnyIterable<U>, W>(callback, ...args));
7583
}
@@ -214,7 +222,9 @@ export class DataStream<T> implements BaseStream<T>, AsyncIterable<T> {
214222
fromStream: W,
215223
onEndYield?: () => { yield: boolean, value?: U }
216224
): DataStream<U> {
217-
return DataStream.from((async function * (stream){
225+
const newStream = this.create<U>();
226+
227+
newStream.read((async function * (stream){
218228
for await (const chunks of stream) {
219229
yield* chunks;
220230
}
@@ -227,6 +237,8 @@ export class DataStream<T> implements BaseStream<T>, AsyncIterable<T> {
227237
}
228238
}
229239
})(fromStream));
240+
241+
return newStream;
230242
}
231243

232244
protected getReader(

src/streams/string-stream.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
11
import { DataStream } from "./data-stream";
2-
import { AnyIterable } from "../types";
2+
import { AnyIterable, TransformFunction } from "../types";
33

44
export class StringStream extends DataStream<string> {
55

6+
create(): StringStream {
7+
return new StringStream();
8+
}
9+
610
split(splitBy: string) {
711
const splitter = this.getSplitter(splitBy);
812
const onEndYield = () => ({ yield: splitter.emitLastValue, value: splitter.lastValue });
913

10-
return this.asNewFlattenedStream<string, DataStream<AnyIterable<string>>>(
14+
return this.asNewFlattenedStream(
1115
this.map<AnyIterable<string>>(splitter.fn),
1216
onEndYield
1317
) as StringStream;
1418
}
1519

20+
filter<W extends any[] = []>(callback: TransformFunction<string, Boolean, W>, ...args: W): StringStream {
21+
return super.filter(callback, ...args) as StringStream;
22+
}
23+
24+
flatMap<W extends any[] = []>(
25+
callback: TransformFunction<string, AnyIterable<string>, W>,
26+
...args: W
27+
): StringStream {
28+
return super.flatMap(callback, ...args) as StringStream;
29+
}
30+
1631
private getSplitter(splitBy: string) {
1732
const result: any = {
1833
emitLastValue: false,

test/streams/string/creation.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,66 @@ test("StringStream can be created via static from method", (t) => {
1212

1313
t.true(stringStream instanceof StringStream);
1414
});
15+
16+
test("StringStream split returns instance of StringStream", (t) => {
17+
const stringStream = StringStream.from(["1", "2", "3", "4"]);
18+
const newStream = stringStream.split("2");
19+
20+
console.log("split", newStream);
21+
22+
t.true(newStream instanceof StringStream, `Should be an instance of StringStream, not ${newStream.constructor.name}`);
23+
t.deepEqual(newStream.constructor.name, "StringStream");
24+
25+
const newStream2 = newStream.split("1");
26+
27+
console.log("split", newStream2);
28+
29+
t.true(newStream2 instanceof StringStream, `Should be an instance of StringStream, not ${newStream2.constructor.name}`);
30+
t.deepEqual(newStream2.constructor.name, "StringStream");
31+
});
32+
33+
test("StringStream flatMap returns instance of StringStream", (t) => {
34+
const stringStream = StringStream.from(["1", "2", "3", "4"]);
35+
const newStream = stringStream.flatMap(chunk => [chunk]);
36+
37+
console.log("flatMap", newStream);
38+
39+
t.true(newStream instanceof StringStream, `Should be an instance of StringStream, not ${newStream.constructor.name}`);
40+
t.deepEqual(newStream.constructor.name, "StringStream");
41+
42+
const newStream2 = newStream.split("1");
43+
44+
console.log("split", newStream2);
45+
46+
t.true(newStream2 instanceof StringStream, `Should be an instance of StringStream, not ${newStream2.constructor.name}`);
47+
t.deepEqual(newStream2.constructor.name, "StringStream");
48+
});
49+
50+
test("StringStream filter returns instance of StringStream", (t) => {
51+
const stringStream = StringStream.from(["1", "2", "3", "4"]);
52+
const newStream = stringStream.filter(chunk => parseInt(chunk, 10) % 2 === 0);
53+
54+
console.log("filter", newStream);
55+
56+
t.true(newStream instanceof StringStream, `Should be an instance of StringStream, not ${newStream.constructor.name}`);
57+
t.deepEqual(newStream.constructor.name, "StringStream");
58+
59+
const newStream2 = newStream.split("1");
60+
61+
console.log("split", newStream2);
62+
63+
t.true(newStream2 instanceof StringStream, `Should be an instance of StringStream, not ${newStream2.constructor.name}`);
64+
t.deepEqual(newStream2.constructor.name, "StringStream");
65+
});
66+
67+
test("StringStream map returns instance of StringStream", (t) => {
68+
const stringStream = StringStream.from(["1", "2", "3", "4"]);
69+
const newStream = stringStream.map(chunk => `foo-${chunk}`);
70+
71+
console.log("map", newStream); // returns StringStream (since it returns this internally)
72+
// newStream.split("3"); // static code analysis does not accept that since
73+
// map should return DataStream (but it works at runtime)
74+
75+
t.true(newStream instanceof StringStream, `Should be an instance of StringStream, not ${newStream.constructor.name}`);
76+
t.deepEqual(newStream.constructor.name, "StringStream");
77+
});

tsconfig.build.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"include": [
1313
"src",
1414
"test",
15-
"bdd"
15+
"bdd",
16+
"samples"
1617
]
1718
}

0 commit comments

Comments
 (0)