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

add overrides for type guard predicates #107

Merged
merged 1 commit into from
Oct 18, 2023
Merged

Conversation

Neuroboy23
Copy link
Contributor

@Neuroboy23 Neuroboy23 commented Oct 18, 2023

Prior to this change, there was no advantage to using a type guard as a predicate in any of the functions that support predicates.

Example for where:

import Enumerable from "../linq";

// the type guard
export function isDefined<T>(value: T | null | undefined): value is T {
    return value !== undefined && value !== null;
}

// `input` is a `(string | null | undefined)[]`
const input = ["foo", null, undefined];

// `output` is a `(string | null | undefined)[]`, which is unexpected because it can only contain strings
const output = Enumerable
    .from(input)
    .where(isDefined)
    .toArray();

With this change, the type of output is string[].

Example for first:

import Enumerable from "../linq";

// the type guard
export function isDefined<T>(value: T | null | undefined): value is T {
    return value !== undefined && value !== null;
}

// `input` is a `(string | null | undefined)[]`
const input = ["foo", null, undefined];

// `output` is a `string | null | undefined`, which is unexpected because it can only be a `string`
const output = Enumerable
    .from(input)
    .first(isDefined)

With this change, the type of output is string.

@Neuroboy23 Neuroboy23 changed the title add where override for type guard predicates add overrides for type guard predicates Oct 18, 2023
singleOrDefault<TDefault>(predicate: (element: T, index: number) => boolean, defaultValue: TDefault): T | TDefault;
singleOrDefault(predicate: (element: T, index: number) => boolean): T | undefined;
singleOrDefault<TDefault>(defaultValue: TDefault): T | TDefault;
singleOrDefault(): T | undefined;
Copy link
Contributor Author

@Neuroboy23 Neuroboy23 Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two fly-by improvements for the ...OrDefault functions here.

  1. It is common to do something like the code below, which uses a different type for the default value than the type of the elements of the input array. However, the pre-existing definitions required that the default value be a T.
const input: number[] = ...;
const output = Enumerable
    .from(input)
    .firstOrDefault(null); // this would not compile because `null` is not a `number`
  1. With the old definitions, the return value of these functions included | undefined even in cases where the default value was supplied.
const input: number[] = ...;
const output = Enumerable // `output` is a `number | undefined`, but it can only possibly be a `number`
    .from(input)
    .firstOrDefault(123);

As I was adding support for type guard predicates, these weaknesses came to the forefront. If I were to follow the pattern of the pre-existing functions, then the new overloads would be like the following. Note that the return type is T | TOther | undefined, which allows for the type guard to work but still confuses the point by insisting that the default has to be a T, and insisting that the return value could be undefined even if a default value is given.

firstOrDefault<TOther extends T>(predicate?: (element: T, index: number) => element is TOther, defaultValue?: T): T | TOther | undefined;

By adding the additional overloads with generic type TDefault, the default value is allowed to be any type appropriate for the call site, whether that be a T, a TOther, a null or anything else the developer uses as the default value. And by changing the optional parameters to explicit overloads, we can adjust the return types for the different scenarios to narrow the type in the cases where the return value cannot possibly be undefined. The end result of all of these changes is is enablement of code like this:

const input: (number | string)[] = ...; // `input` is a `(number | string)[]`, something like `[23, 42, "foo", -1, 452]`
const output = Enumerable // all good! `output` is a `number | null`
    .from(input)
    .where(isNumber)
    .firstOrDefault(value => value > 0, null);

@Neuroboy23
Copy link
Contributor Author

Apologies for all the force-pushes. I spotted some nuances in the types that needed additional ironing out. I am now done with this commit.

@mihaifm mihaifm merged commit d9a8efb into mihaifm:master Oct 18, 2023
@mihaifm
Copy link
Owner

mihaifm commented Oct 18, 2023

Nice, thank you. I'll release it later this week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants