5
\$\begingroup\$

I'm wondering and actually playing in typescript playground with generics functions. The question is, is the code below readable? If not what kind of approach I could use to make it more typescript way and more readable?

Working example here

I implemented some basic functions like foldl, map and filter to experiment generics, maybe the implementation don't satisfies all requirements in the sense of functionality.

type FoldlFunction<T> = (a: T, b: T) => T;

function Foldl<T>(f: FoldlFunction<T>, base: T, arr: T[]): T {
    const [head, ...rest] = arr;
    return !head ? base : Foldl(f, f(base, head), rest);
}

console.log(Foldl((a: number, b: number) => (a + b), 0, [1,2,3,4,12,14,15,16]));

type MMapFunction<T,R> = (a: T) => R;

function MMap<T,R>(f: MMapFunction<T,R>, arr: T[]): R[] {
    const [head, ...rest] = arr;
    return !head ? [] : [f(head), ...MMap(f, rest)];
}

console.log(MMap((x: number) => (x * x), [1,2,3,4,12,14,15,16]));

type FilterFunction<T> = (a: T) => boolean;

function Filter<T>(f: FilterFunction<T>, arr: T[]): T[] {
    const [head, ...rest] = arr;
    return !head ? [] : 
        (f(head) ? [head, ...Filter(f, rest)] : [...Filter(f, rest)])
}

console.log(Filter((x: number) => (x % 2 === 0), [1,2,3,4,12,14,15,16]));
\$\endgroup\$

1 Answer 1

6
\$\begingroup\$

Your code looks quite reasonable, though there are a few tweaks you could consider.

Function or Callback? Rather than names like FoldlFunction and MMapFunction, since what those types contain are the types for the callback run for each element of the array, consider calling them FoldlCallback etc instead. It's a bit more precise and will make more intuitive sense at a glance. (One might think at a glance that FoldlFunction might refer to the type of the whole Foldl function, which is incorrect.)

camelCase is the near-universal convention for plain non-class functions and variables in JavaScript. PascalCase is generally used for class constructors and namespaces, which aren't being used here, so camelCase is probably more appropriate. Eg Foldl -> foldl (or to foldL).

DRYer types You only need to specify the type of a parameter when TypeScript can't infer it automatically. For example, with foldl, since the base value is T, and the array is T[], the callback - the f: FoldlFunction<T> - will be known to be of the form (a: T, b: T) => T. There's no need to additionally specify the arguments' types when calling foldl. That is, this:

foldl((a: number, b: number) => (a + b), 0, [1,2,3,4,12,14,15,16])

can be

foldl((a, b) => (a + b), 0, [1,2,3,4,12,14,15,16])

It's a good idea to let TypeScript automatically infer the types of variables and parameters whenever possible - less code to read makes comprehension easier. You can do the same thing for MMap and Filter.

Outside definition? I'm not entirely convinced of defining the type of the function/callback outside - it's one more type parameter that needs to be cognitively pieced together. Consider defining the callback type in the parameter list:

function foldl<T>(
    f: (a: T, b: T) => T,
    base: T,
    arr: T[]
): T {
    const [head, ...rest] = arr;
    return !head ? base : foldl(f, f(base, head), rest);
}

console.log(foldl((a, b) => (a + b), 0, [1,2,3,4,12,14,15,16]));

I consider that significantly more readable. You can do the same thing for MMap and Filter.

Falsey bug You use return !head ? base : foldl.... This will not run if head is falsey. For example, foldl((a, b) => (a + b), 0, [1,2,3,4,12,14,15,16]) results in 67, but foldl((a, b) => (a + b), 0, [0, 1,2,3,4,12,14,15,16]) results in 0, which probably isn't desirable. Check the array's length instead:

function foldl<T>(
    f: (a: T, b: T) => T,
    base: T,
    arr: T[]
): T {
    if (!arr.length) {
        return base;
    }
    const [head, ...rest] = arr;
    return foldl(f, f(base, head), rest);
}

This applies to MMap and Filter as well.

DRYer filter To fix the falsey bug and make Filter a bit cleaner, you can replace:

const [head, ...rest] = arr;
return !head ? [] : 
    (f(head) ? [head, ...Filter(f, rest)] : [...Filter(f, rest)])

with

if (!arr.length) {
  return [];
}
const includeThis = f(head); // call this here to preserve callback execution order
const restFiltered = Filter(f, rest);
return includeThis ? [head, ...restFiltered] : restFiltered;
\$\endgroup\$
6
  • \$\begingroup\$ Thanks for you detailed answer, as a beginner in typescript I really thought that will be good practice to put types explicitly everywhere, good to know that I can trust typescript to infer obvious code by itself. \$\endgroup\$
    – drocha87
    Commented Oct 17, 2020 at 17:54
  • \$\begingroup\$ Other question that arises in my mind, is if it is possible to "extract" the type of an function parameter, for example, you said that the outside definition was not quite relevant in this example, but suppose I want to extract the type of f parameter of foldl will it be possible? I don't know if I make myself understandable, but I know that I can "extract" the type of an object property using keyof right, is there some operator that could help me do the same thing with a function parameter? \$\endgroup\$
    – drocha87
    Commented Oct 17, 2020 at 17:54
  • \$\begingroup\$ You can use Parameters, eg type Callback = Parameters<typeof f>; or Parameters<typeof foldl>[0]; \$\endgroup\$ Commented Oct 17, 2020 at 18:07
  • \$\begingroup\$ Something interesting just happens, I think as typescript cannot infer the contextual type, when I do something like you suggestedtype Callback = Parameters<typeof foldl>[0] the signature of Callback is (a: unknown, b: unknown): unknown as demonstrated here in playground how could I use generics in Callback definition? \$\endgroup\$
    – drocha87
    Commented Oct 17, 2020 at 18:24
  • \$\begingroup\$ Not entirely sure what's going on there. It could be that if the callback type is inlined like that, it can't be extracted outside the function. It can be used inside the function: type Callback = typeof f; If you need the type outside the function, you can declare it outside as well like your original code does with type FoldlFunction<T> = (a: T, b: T) => T;. \$\endgroup\$ Commented Oct 17, 2020 at 21:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.