Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Polymorphic/generic this as return type for subclasses #28

Merged
merged 4 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions samples/generic-this-1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// This is a sample file demonstarting the approached used for "polymorphic this"
// for generic method inheritance. It can be run with:
//
// npm run build && node build/samples/generic-this-1.js"

class BaseClass<T> {
public value: T;

constructor(value: T) {
this.value = value;
}

create(value: T): BaseClass<T>;
create<U>(value: U): BaseClass<U>;
create<U extends any>(value: U): BaseClass<U> {
return new BaseClass<U>(value);
}

method1(): this {
return this;
}

method2(): BaseClass<T> {
return this;
}

method3(value: T): BaseClass<T> {
return this.create<T>(value);
}

method5(value: T): BaseClass<T>;
method5<U>(value: U): BaseClass<U>;
method5<U extends any>(value: U): BaseClass<U> {
return this.create<U>(value);
}
}

class DerivedClass<T> extends BaseClass<T> {
create(value: T): DerivedClass<T>;
create<U>(value: U): DerivedClass<U>;
create<U extends any>(value: U): DerivedClass<U> {
return new DerivedClass<U>(value);
}

method2(): DerivedClass<T> {
return super.method2() as DerivedClass<T>;
}

method3(value: T): DerivedClass<T> {
return super.method3(value) as DerivedClass<T>;
}

method5(value: T): DerivedClass<T>;
method5<U>(value: U): DerivedClass<U>;
method5<U>(value: U): DerivedClass<U> {
return super.method5(value) as DerivedClass<U>;
}

ownMethod1() {
console.log(this);
}
}

class DerivedClassFixedType extends DerivedClass<number> {
create(value: number): DerivedClassFixedType {
return new DerivedClassFixedType(value);
}

method2(): DerivedClassFixedType {
return super.method2() as DerivedClassFixedType;
}

method3(value: number): DerivedClassFixedType {
return super.method3(value) as DerivedClassFixedType;
}

method5(value: number): DerivedClassFixedType {
return super.method5(value) as DerivedClassFixedType;
}

ownMethod2() {
console.log(this);
}
}

class BaseClassFixedType extends BaseClass<string> {
create(value: string): BaseClassFixedType {
return new BaseClassFixedType(value);
}

method2(): BaseClassFixedType {
return super.method2() as BaseClassFixedType;
}

method3(value: string): BaseClassFixedType {
return super.method3(value) as BaseClassFixedType;
}

method5(value: string): BaseClassFixedType {
return super.method5(value) as BaseClassFixedType;
}

ownMethod3() {
console.log(this);
}
}

// --- BaseClass

const bcString = new BaseClass("foo");
const bcString1 = bcString.method1();
const bcString2 = bcString.method2();
const bcString3 = bcString.method3("bar");
const bcString5 = bcString.method5(123);

for (const instance of [bcString, bcString1, bcString2, bcString3, bcString5]) {
console.log(`instanceof: ${ instance instanceof BaseClass }; constructor.name: ${ instance.constructor.name }`);
}

// --- DerivedClass

const dcString = new DerivedClass("foo");
const dcString1 = dcString.method1();
const dcString2 = dcString.method2();
const dcString3 = dcString.method3("bar");
const dcString5 = dcString.method5(123);

for (const instance of [dcString, dcString1, dcString2, dcString3, dcString5]) {
console.log(`instanceof: ${ instance instanceof DerivedClass }; constructor.name: ${ instance.constructor.name }`);
}

dcString.ownMethod1();
dcString1.ownMethod1();
dcString2.ownMethod1();
dcString3.ownMethod1();
dcString5.ownMethod1();

// --- DerivedClassFixedType

const dcftString = new DerivedClassFixedType(123);
const dcftString1 = dcftString.method1();
const dcftString2 = dcftString.method2();
const dcftString3 = dcftString.method3(456);
const dcftString5 = dcftString.method5(123);

for (const instance of [dcftString, dcftString1, dcftString2, dcftString3, dcftString5]) {
console.log(`instanceof: ${ instance instanceof DerivedClassFixedType }; constructor.name: ${ instance.constructor.name }`);
}

dcftString.ownMethod2();
dcftString1.ownMethod2();
dcftString2.ownMethod2();
dcftString3.ownMethod2();
dcftString5.ownMethod2();

// --- BaseClassFixedType

const bcftString = new BaseClassFixedType("foo");
const bcftString1 = bcftString.method1();
const bcftString2 = bcftString.method2();
const bcftString3 = bcftString.method3("bar");
const bcftString5 = bcftString.method5("baz");
// const toBcStrign6 = bcftString.method5(123); // This won't work for now

for (const instance of [bcftString, bcftString1, bcftString2, bcftString3, bcftString5]) {
console.log(`instanceof: ${ instance instanceof BaseClassFixedType }; constructor.name: ${ instance.constructor.name }`);
}

bcftString.ownMethod3();
bcftString1.ownMethod3();
bcftString2.ownMethod3();
bcftString3.ownMethod3();
bcftString5.ownMethod3();
14 changes: 13 additions & 1 deletion src/streams/data-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export class DataStream<T> implements BaseStream<T>, AsyncIterable<T> {
};
}

create(): DataStream<T>;
create<U>(): DataStream<U>;
create<U>(): DataStream<U> {
return new DataStream<U>();
}

map<U, W extends any[] = []>(callback: TransformFunction<T, U, W>, ...args: W): DataStream<U> {
if (args?.length) {
this.ifca.addTransform(this.injectArgsToCallback<U, typeof args>(callback, args));
Expand All @@ -70,6 +76,8 @@ export class DataStream<T> implements BaseStream<T>, AsyncIterable<T> {
return this;
}

flatMap<W extends any[] = []>(callback: TransformFunction<T, AnyIterable<T>, W>, ...args: W): DataStream<T>;
flatMap<U, W extends any[] = []>(callback: TransformFunction<T, AnyIterable<U>, W>, ...args: W): DataStream<U>
flatMap<U, W extends any[] = []>(callback: TransformFunction<T, AnyIterable<U>, W>, ...args: W): DataStream<U> {
return this.asNewFlattenedStream(this.map<AnyIterable<U>, W>(callback, ...args));
}
Expand Down Expand Up @@ -214,7 +222,9 @@ export class DataStream<T> implements BaseStream<T>, AsyncIterable<T> {
fromStream: W,
onEndYield?: () => { yield: boolean, value?: U }
): DataStream<U> {
return DataStream.from((async function * (stream){
const newStream = this.create<U>();

newStream.read((async function * (stream){
for await (const chunks of stream) {
yield* chunks;
}
Expand All @@ -227,6 +237,8 @@ export class DataStream<T> implements BaseStream<T>, AsyncIterable<T> {
}
}
})(fromStream));

return newStream;
}

protected getReader(
Expand Down
19 changes: 17 additions & 2 deletions src/streams/string-stream.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
import { DataStream } from "./data-stream";
import { AnyIterable } from "../types";
import { AnyIterable, TransformFunction } from "../types";

export class StringStream extends DataStream<string> {

create(): StringStream {
return new StringStream();
}

split(splitBy: string) {
const splitter = this.getSplitter(splitBy);
const onEndYield = () => ({ yield: splitter.emitLastValue, value: splitter.lastValue });

return this.asNewFlattenedStream<string, DataStream<AnyIterable<string>>>(
return this.asNewFlattenedStream(
this.map<AnyIterable<string>>(splitter.fn),
onEndYield
) as StringStream;
}

filter<W extends any[] = []>(callback: TransformFunction<string, Boolean, W>, ...args: W): StringStream {
return super.filter(callback, ...args) as StringStream;
}

flatMap<W extends any[] = []>(
callback: TransformFunction<string, AnyIterable<string>, W>,
...args: W
): StringStream {
return super.flatMap(callback, ...args) as StringStream;
}

private getSplitter(splitBy: string) {
const result: any = {
emitLastValue: false,
Expand Down
63 changes: 63 additions & 0 deletions test/streams/string/creation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,66 @@ test("StringStream can be created via static from method", (t) => {

t.true(stringStream instanceof StringStream);
});

test("StringStream split returns instance of StringStream", (t) => {
const stringStream = StringStream.from(["1", "2", "3", "4"]);
const newStream = stringStream.split("2");

console.log("split", newStream);

t.true(newStream instanceof StringStream, `Should be an instance of StringStream, not ${newStream.constructor.name}`);
t.deepEqual(newStream.constructor.name, "StringStream");

const newStream2 = newStream.split("1");

console.log("split", newStream2);

t.true(newStream2 instanceof StringStream, `Should be an instance of StringStream, not ${newStream2.constructor.name}`);
t.deepEqual(newStream2.constructor.name, "StringStream");
});

test("StringStream flatMap returns instance of StringStream", (t) => {
const stringStream = StringStream.from(["1", "2", "3", "4"]);
const newStream = stringStream.flatMap(chunk => [chunk]);

console.log("flatMap", newStream);

t.true(newStream instanceof StringStream, `Should be an instance of StringStream, not ${newStream.constructor.name}`);
t.deepEqual(newStream.constructor.name, "StringStream");

const newStream2 = newStream.split("1");

console.log("split", newStream2);

t.true(newStream2 instanceof StringStream, `Should be an instance of StringStream, not ${newStream2.constructor.name}`);
t.deepEqual(newStream2.constructor.name, "StringStream");
});

test("StringStream filter returns instance of StringStream", (t) => {
const stringStream = StringStream.from(["1", "2", "3", "4"]);
const newStream = stringStream.filter(chunk => parseInt(chunk, 10) % 2 === 0);

console.log("filter", newStream);

t.true(newStream instanceof StringStream, `Should be an instance of StringStream, not ${newStream.constructor.name}`);
t.deepEqual(newStream.constructor.name, "StringStream");

const newStream2 = newStream.split("1");

console.log("split", newStream2);

t.true(newStream2 instanceof StringStream, `Should be an instance of StringStream, not ${newStream2.constructor.name}`);
t.deepEqual(newStream2.constructor.name, "StringStream");
});

test("StringStream map returns instance of StringStream", (t) => {
const stringStream = StringStream.from(["1", "2", "3", "4"]);
const newStream = stringStream.map(chunk => `foo-${chunk}`);

console.log("map", newStream); // returns StringStream (since it returns this internally)
// newStream.split("3"); // static code analysis does not accept that since
// map should return DataStream (but it works at runtime)

t.true(newStream instanceof StringStream, `Should be an instance of StringStream, not ${newStream.constructor.name}`);
t.deepEqual(newStream.constructor.name, "StringStream");
});
3 changes: 2 additions & 1 deletion tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"include": [
"src",
"test",
"bdd"
"bdd",
"samples"
]
}