Skip to content

Commit

Permalink
Merge pull request #2 from vojtatranta/fix-missing-undefined-part
Browse files Browse the repository at this point in the history
fix the missing undefined branch of nullable
  • Loading branch information
vojtatranta authored Dec 11, 2024
2 parents cf5751e + 69feae4 commit d0a591f
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 10 deletions.
67 changes: 67 additions & 0 deletions src/Maybe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ describe("Maybe", () => {
expect(result).toEqual("b");
});

it("should narrow a chained value properly", () => {
const objectWithFalsy: {
falsyValue: string | null | undefined;
}[] = [
{
falsyValue: "string",
},
];

const result = Maybe.of(objectWithFalsy[0])
.map(({ falsyValue }) => falsyValue)
.map((value: string) => value)
.getValue("not-string");

expect(result).toEqual("string");
});

it("should return original when default parameter is passed to getValue()", () => {
const a = "a" as const;
const b = "b" as const;
Expand All @@ -66,6 +83,16 @@ describe("Maybe", () => {
const result: "a" | undefined | null = Maybe.of(a).getValue();
expect(result).toEqual("a");
});
it("should narrow undefined value to the truthy value in map()", () => {
const a = "a" as const;

const result: "a" | undefined | null = Maybe.of(a)
.map((a: "a") => {
return a;
})
.getValue();
expect(result).toEqual("a");
});

it("should return original when default parameter is passed to getValue() with null", () => {
const a = "a" as const;
Expand Down Expand Up @@ -113,6 +140,29 @@ describe("Maybe", () => {
expect(value).toEqual("c");
callback();
});

it("should narrow nullable value in to non nullable", (callback) => {
const nullableA: string | null | undefined = "a";
const value = Maybe.all([nullableA])
.map(([truthyA]: [string]) => truthyA)
.getValue();

expect(value).toEqual("a");
callback();
});

it("should narrow multiple nullable values in to non nullable", (callback) => {
const nullableA: string | null = "a";
const nullableB: string | null = "b";
const value = Maybe.all([nullableA, nullableB])
.map(([truthyA, truthyB]: [string, string]) => {
return "a";
})
.getValue("a");

expect(value).toEqual("a");
callback();
});
});
});

Expand Down Expand Up @@ -194,6 +244,23 @@ describe("Maybe", () => {
expect(result.getValue()).toEqual("a");
});

it("should narrow a chained value properly", () => {
const objectWithFalsy: {
falsyValue: string | null | undefined;
}[] = [
{
falsyValue: "string",
},
];

const result = Maybe.fromFirst(objectWithFalsy)
.map(({ falsyValue }) => falsyValue)
.map((value: string) => value)
.getValue("not-string");

expect(result).toEqual("string");
});

it("should mantain the constant type of the first element", () => {
const result = Maybe.fromFirst([
"a" as const,
Expand Down
19 changes: 9 additions & 10 deletions src/Maybe.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
type NonNullableMaybe<T, D = never> = T extends Maybe<infer U>
? U extends null
? U extends null | undefined
? 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
Expand All @@ -27,7 +26,7 @@ export class Maybe<T> {
}

static asInput<NV, R>(
callback: (input: NonNullablePart<NV>) => R
callback: (input: NonNullable<NV>) => R
): (externalInput: NV) => Maybe<R> {
return (externalInput: NV): Maybe<R> =>
Maybe.of<NV>(externalInput).map(callback);
Expand Down Expand Up @@ -77,28 +76,28 @@ export class Maybe<T> {
return Maybe.of<T[N]>(someArray?.[n]);
}

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

return new Maybe<NonNullablePart<R>>(null);
return new Maybe<NonNullable<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: NonNullablePart<T>) => U
mapper: (value: NonNullable<T>) => U
): 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 as NonNullablePart<T>);
const result = mapper(this.value as NonNullable<T>);

if (result instanceof Maybe) {
return new Maybe<U>(result.getValue(this.value)) as LocalReturn;
Expand All @@ -111,7 +110,7 @@ export class Maybe<T> {
}

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

0 comments on commit d0a591f

Please sign in to comment.