Armin Bu
Armin Bu

Reputation: 1360

Use an explicit return of undefined as type guard and/or filter?

I am trying to combine the array methods filter and map into one function called malter. I have come so far:

type mapFn<T, S> = (value: T, index: number, originalArray: Readonly<T[]>) => S;

interface Array<T> {
  malter<S = any>(mapFn: mapFn<T, S>): S[];
}

function notUndefined<T>(v: T | undefined): v is T {
  return typeof v !== "undefined"
}

Array.prototype.malter = function malter<T, S = any>(mapFn: mapFn<T, S>): S[] {
  return this.reduce(function(acc: S[], val: T, index: number, orgArr: T[]) {
    const el = mapFn(val, index, orgArr);
    if (notUndefined(el)) {
      acc.push(el);
    }
    return acc;
  }, []);
};

It basically works. But when using it, it throws a TypeError in line 5-7. Another test function that did implicitly return undefined did also throw that error.

Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'."
const test = [2, 3, 4, 5, 3];

function test1(): string[] {
   return test.malter<number, string>(num => 
      num > 3 
       ? num.toFixed(2) 
       : undefined
   );
}

A solution that would work type savely, is to provide 2 arguments for malter. A filter and a map function and call them seperately. That would ensure type savety, but also make it less simple.

Sure I could simply do that in line 5-7:

(num > 3 ? num.toFixed(2) : undefined) as string

It may be the best compromise? What do you think? Is there a solution I didn't think of, or do you go with a compromise?

Upvotes: 1

Views: 316

Answers (2)

jcalz
jcalz

Reputation: 327754

@TitianCernicova-Dragomir's answer is correct, but I'd also like to give a slightly different solution. The main difference is that instead of using conditional types to turn S (which possibly includes undefined) into to Exclude<S, undefined> (which doesn't), we take S as the return type (which doesn't include undefined) and use the input type as S | undefined. They will act the same (or nearly so) from the caller's point of view (the compiler will do its own Exclude-like analysis on the input type), but the compiler will likely be able to reason about types inside the implementation of malter better in the latter case:

type mapFn<T, S> = (value: T, index: number, originalArray: Readonly<T[]>) => S;

interface Array<T> {
  // S will not include undefined when inferred from mapFn
  malter<S>(mapFn: mapFn<T, S | undefined>): S[];
}

// hey, look, this works the same way, with T|undefined => T
// instead of T => Exclude<T, undefined>
function notUndefined<T>(v: T | undefined): v is T {
  return typeof v !== "undefined";
}

Array.prototype.malter = function malter<T, S>(
  this: Array<T>, // inform the compiler that this is an array
  mapFn: mapFn<T, S | undefined> 
): S[] {
  return this.reduce(function(acc: S[], val: T, index: number, orgArr: T[]) {
    const el = mapFn(val, index, orgArr);
    if (notUndefined(el)) {
      acc.push(el);
    }
    return acc;
  }, []);
}; // inference in implementation works


const test = [2, 3, 4, 5, 3];

// don't need any type parameters 
function test1(): string[] {
  return test.malter(num => (num > 3 ? num.toFixed(2) : undefined));
}

Okay, hope that helps. Good luck!

Link to code

Upvotes: 2

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249486

You are using undefined as the value to be filtered. We can allow the inner function to return undefined and then filter it out using Exclude, much like the implementation does:

type mapFn<T, S> = (value: T, index: number, originalArray: Readonly<T[]>) => S;

interface Array<T> {
    malter<S>(mapFn: mapFn<T, S>): Exclude<S, undefined>[];
}

function notUndefined<T>(v: T | undefined): v is T {
    return typeof v !== "undefined"
}

Array.prototype.malter = function malter<T, S>(mapFn: mapFn<T, S>): Exclude<S, undefined>[] {
    return this.reduce(function (acc: S[], val: T, index: number, orgArr: T[]) {
        const el = mapFn(val, index, orgArr);
        if (notUndefined(el)) {
            acc.push(el);
        }
        return acc;
    }, []);
};

const test = [2, 3, 4, 5, 3];

function test1(): string[] {
    return test.malter(num =>
        num > 3
            ? num.toFixed(2)
            : undefined
    );
}

Upvotes: 1

Related Questions