HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptMajor

Filtering undefined elements out of an array

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
elementsarrayundefinedoutfiltering

Problem

I have some code that filters an array with type (Suggestion|undefined)[] into an array with type Suggestion[], but I feel it does so very inelegantly:

const optionsProvider: AutocompletionProvider = () => {
    const results: (Suggestion|undefined)[] = flagDescriptions.map(descriptions => {
        let shortFlagWithArgument = descriptions[0].match(/^ *-(\w) (\w*)$/);
        let shortFlagWithoutArgument = descriptions[0].match(/^ *-(\w) *(.*)$/);
        if (shortFlagWithArgument) {
            const flag = shortFlagWithArgument[1];
            const argument = shortFlagWithArgument[2];
            const description = combineManPageLines(descriptions.slice(1));

            return new Suggestion({
                value: `-${flag}`,
                style: styles.option,
                description,
                displayValue: `-${flag} ${argument}`,
            });
        } else if (shortFlagWithoutArgument) {
            const flag = shortFlagWithoutArgument[1];
            const description = combineManPageLines([shortFlagWithoutArgument[2], ...descriptions.slice(1)]);

            return new Suggestion({
                value: `-${flag}`,
                style: styles.option,
                description,
            });
        }
    });
    const filteredResults: Suggestion[] = [];
    results.forEach(result => {
        if (result) {
            filteredResults.push(result);
        }
    });
    return filteredResults;
};


In straight JS, this would just be array.filter(element => element !== undefined) but the best working solution I could come up with in TypeScript was to push the defined elements into a separate array within a forEach.

For context:

type AutocompletionProvider = (context: AutocompletionContext) => Suggestion[] | Promise;


Original PR: https://github.com/shockone/black-screen/pull/623/files

Solution

To be using strict null checks, you must be on "typescript": "^2.0.0". Thus, my example will be using this version of typescript with "compilerOptions":{"strictNullChecks": true}.

It does seem that the filter solution is the most elegant solution to the titular question. Sadly, it seems that the typescript static analysis is unable to track this behavior:

const source: (string|undefined)[] = ["one", "two", "three", undefined, "five"]
const filtered: string[] = source.filter(element => element !== undefined)
// [ts]
// Type '(string | undefined)[]' is not assignable to type 'string[]'.
// Type 'string | undefined' is not assignable to type 'string'.
// Type 'undefined' is not assignable to type 'string'.


This seems to be the case because of the declaration of the filter method:

/** Returns the elements of an array that meet the condition specified in a callback function. */
(method) Array.filter(callbackfn: (value: string | undefined, index: number, array: (string | undefined)[]) => any, thisArg?: any): (string | undefined)[]


In this case, our own type logic beats out the compiler's. It's these cases that is the reason that Type Assertion is part of the language spec. Thus:

const source: (string|undefined)[] = ["one", "two", "three", undefined, "five"]
const filtered: string[] = source.filter(element => element !== undefined) as string[]


This transpiles exactly as you would expect -- in this case, transpiling is as simple as just removing the type information. If you need to convince the language to convert two unrelated types, you need to step through any to tell the compiler to back off: object as any as string.

Now, looking at the rest of the code:

shortFlagWithArgument and shortFlagWithoutArgument can be consts, as you do not mutate their values.

Personally, I would add an explicit final return to the flagDescriptions.map operation. This makes explicit why the undefined needs to be in the results array in the first place.

The regex patterns seem good candidates for refactoring into a reusable constant. Depending on your project ecosystem, however, it may be better just to leave them inlined.

Context

StackExchange Code Review Q#135363, answer score: 23

Revisions (0)

No revisions yet.