Skip to content

Commit

Permalink
Merge pull request #1 from vojtatranta/array-methods
Browse files Browse the repository at this point in the history
Array methods
  • Loading branch information
vojtatranta authored Nov 26, 2024
2 parents 272f404 + 6acb7f3 commit b8070c0
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 50 deletions.
27 changes: 13 additions & 14 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
name: Test workflow
on:
push:
branches:
- master
branches: ["*"]

jobs:
test-job:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3

- name: Use Node 17.x
uses: actions/setup-node@v3
with:
node-version: '17.x'
- name: Use Node 17.x
uses: actions/setup-node@v3
with:
node-version: "17.x"

- name: Install dependencies
run: npm ci
- name: Install dependencies
run: npm ci

- name: Test
run: npm test
- name: Test
run: npm test

- name: Types
run: npm run typecheck
- name: Types
run: npm run typecheck
120 changes: 120 additions & 0 deletions src/Maybe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,125 @@ describe("Maybe", () => {
const result: Maybe<number> = inputAsMaybeFunc("test");
});
});

describe("extraction of array elements", () => {
describe("fromFirst()", () => {
it("fromFirst()", () => {
const result: Maybe<string> = Maybe.fromFirst(["a", "b", "c"]);

expect(result.getValue()).toEqual("a");
});

it("should mantain the constant type of the first element", () => {
const result = Maybe.fromFirst([
"a" as const,
"b" as const,
"c" as const,
] as const);

result.map((value: "a") => {
expect(value).toEqual("a");
});
});

it("should mantain the type of the first element", (callback) => {
const result = Maybe.fromFirst(["a", "b", "c"]);

result.map((value: string) => {
expect(value).toEqual("a");
callback();
});
});

it("should apply the default value when the first element is null", () => {
const result = Maybe.fromFirst([null, "b", "c"]);

const dConst: string = result.getValue("d");

expect(dConst).toEqual("d");
});
});

describe("fromLast()", () => {
it("should get the last element", () => {
const result: Maybe<string> = Maybe.fromLast(["a", "b", "c"]);

expect(result.getValue()).toEqual("c");
});

it("should map the last element", (callback) => {
const result = Maybe.fromLast([
"a" as const,
"b" as const,
"c" as const,
]);

result.map((value: "c") => {
expect(value).toEqual("c");
callback();
});
});

it("should apply the default value when the last element is null", () => {
const result = Maybe.fromLast(["a", "b", null]);

expect(result.getValue("d")).toEqual("d");
});

it("should apply the default value when the last element is undefined", () => {
const result = Maybe.fromLast(["b", "c", undefined]);

expect(result.getValue("X")).toEqual("X");
});

it("should mantain the constant type of the last element", (callback) => {
const result = Maybe.fromLast([
"a" as const,
"b" as const,
"c" as const,
] as const);

result.map((value: "c") => {
expect(value).toEqual("c");
callback();
});
});
});

describe("fromNth()", () => {
it("should get the nth element", () => {
const result: Maybe<string> = Maybe.fromNth(["a", "b", "c"], 1);

expect(result.getValue()).toEqual("b");
});

it("should map the nth element", (callback) => {
const result = Maybe.fromNth(
["a" as const, "b" as const, "c" as const],
1
);

result.map((value: string) => {
expect(value).toEqual("b");
callback();
});
});

it("should apply the default value when the nth element is null", () => {
const result = Maybe.fromNth(["a", "b", null], 2);

expect(result.getValue("X")).toEqual("X");
});

it("should mantain the type of the nth element in a mixed array", (callback) => {
const result = Maybe.fromNth(["a", "b", 123, null], 2);

result.map((value: string | number) => {
expect(value).toEqual(123);
callback();
});
});
});
});
});
});
114 changes: 78 additions & 36 deletions src/Maybe.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,126 @@
type NonNullableMaybe<T, D = never> = T extends Maybe<infer U> ? U extends null ? D : Maybe<U> : D;
type NonNullablePart<T> = T extends null ? never : T
type UnpackedMaybe<T> = T extends Maybe<infer U> ? U : T
type NonNullableMaybe<T, D = never> = T extends Maybe<infer U>
? U extends null
? D
: Maybe<U>
: D;
type NonNullablePart<T> = T extends null ? never : T;
type UnpackedMaybe<T> = T extends Maybe<infer U> ? U : T;
type Last<T extends any[] | readonly any[]> = T extends [...infer Rest, infer L]
? L
: never;

export class Maybe<T> {
private value: T | null | undefined
private value: T | null | undefined;

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

static of<NV>(value: NV | null | undefined) {
return new Maybe<NV>(value)
return new Maybe<NV>(value);
}

static asMaybeInput<NV, R>(callback: (input: Maybe<NV>) => R): (externalInput: NV) => R {
return (externalInput: NV): R => callback(Maybe.of<NV>(externalInput))
static asMaybeInput<NV, R>(
callback: (input: Maybe<NV>) => R
): (externalInput: NV) => R {
return (externalInput: NV): R => callback(Maybe.of<NV>(externalInput));
}

static asInput<NV, R>(callback: (input: NonNullable<NV>) => R): (externalInput: NV) => Maybe<R> {
return (externalInput: NV): Maybe<R> => Maybe.of<NV>(externalInput).map(callback)
static asInput<NV, R>(
callback: (input: NonNullablePart<NV>) => R
): (externalInput: NV) => Maybe<R> {
return (externalInput: NV): Maybe<R> =>
Maybe.of<NV>(externalInput).map(callback);
}

static all<T extends unknown[]>(maybies: [...T]): Maybe<{ -readonly [P in keyof T]: UnpackedMaybe<T[P]> }> {
const results: unknown[] = []
static all<T extends unknown[]>(
maybies: [...T]
): Maybe<{ -readonly [P in keyof T]: UnpackedMaybe<T[P]> }> {
const results: unknown[] = [];

const allMaybies = maybies.map((value) => value instanceof Maybe ? value : Maybe.of(value))
allMaybies.forEach((maybeValue) => maybeValue.map((truthyValue) => {
results.push(truthyValue)
}))
const allMaybies = maybies.map((value) =>
value instanceof Maybe ? value : Maybe.of(value)
);
allMaybies.forEach((maybeValue) =>
maybeValue.map((truthyValue) => {
results.push(truthyValue);
})
);

if (results.length === maybies.length) {
return Maybe.of(results) as Maybe<{ -readonly [P in keyof T]: UnpackedMaybe<T[P]> }>
return Maybe.of(results) as Maybe<{
-readonly [P in keyof T]: UnpackedMaybe<T[P]>;
}>;
}

return Maybe.of(null as unknown as { -readonly [P in keyof T]: UnpackedMaybe<T[P]> })
return Maybe.of(
null as unknown as { -readonly [P in keyof T]: UnpackedMaybe<T[P]> }
);
}

map<R>(mapper: (value: NonNullable<T>) => R): Maybe<NonNullablePart<R>> {
static fromFirst<T extends any[] | readonly any[]>(
someArray: T | null | undefined
): Maybe<T[0]> {
return Maybe.fromNth<T, 0>(someArray, 0);
}

static fromLast<T extends any[] | readonly any[]>(
someArray: T | null | undefined
): Maybe<Last<T>> {
return Maybe.fromNth(someArray, (someArray?.length ?? 0) - 1);
}

static fromNth<T extends any[] | readonly any[], N extends number>(
someArray: T | null | undefined,
n: N
): Maybe<T[N]> {
return Maybe.of<T[N]>(someArray?.[n]);
}

map<R>(mapper: (value: NonNullablePart<T>) => R): Maybe<NonNullablePart<R>> {
if (this.value != null) {
return new Maybe<NonNullablePart<R>>(
mapper(this.value) as NonNullablePart<R>
)
mapper(this.value as NonNullablePart<T>) as NonNullablePart<R>
);
}

return new Maybe<NonNullablePart<R>>(null)
return new Maybe<NonNullablePart<R>>(null);
}

// NOTE: flatMap() "eats" the incomming maybe and will use the value of the current maybe as default
// of the maybe returned by the Mapper()
// you can safely use Maybies in the flatMap() function return value as if it was a direct value
flatMap<U>(
mapper: (value: NonNullable<T>) => U
mapper: (value: NonNullablePart<T>) => U
): NonNullableMaybe<U extends Maybe<infer R> ? Maybe<R> : Maybe<U>> {
type LocalReturn = NonNullableMaybe<U extends Maybe<infer R> ? Maybe<R> : Maybe<U>>
type LocalReturn = NonNullableMaybe<
U extends Maybe<infer R> ? Maybe<R> : Maybe<U>
>;

if (this.value != null) {
const result = mapper(this.value)
const result = mapper(this.value as NonNullablePart<T>);

if (result instanceof Maybe) {
return new Maybe<U>(result.getValue(this.value)) as LocalReturn
return new Maybe<U>(result.getValue(this.value)) as LocalReturn;
}

return new Maybe<U>(result) as LocalReturn
return new Maybe<U>(result) as LocalReturn;
}

return new Maybe<U>(null) as LocalReturn
return new Maybe<U>(null) as LocalReturn;
}

getValue(): T | null | undefined
getValue<D>(defaultValue: D): NonNullable<T> | D
getValue(): T | null | undefined;
getValue<D>(defaultValue: D): NonNullablePart<T> | D;
getValue<D>(defaultValue?: D): T | D | null | undefined {
if (this.value) {
return this.value
return this.value;
}

if (typeof defaultValue !== 'undefined') {
return defaultValue
if (typeof defaultValue !== "undefined") {
return defaultValue;
}
return this.value

return this.value;
}
}

0 comments on commit b8070c0

Please sign in to comment.