Skip to content

Instantly share code, notes, and snippets.

@kaw2k
Last active November 2, 2017 15:43
Show Gist options
  • Save kaw2k/bf8373f1eaf00cf6692b3a4cab554ade to your computer and use it in GitHub Desktop.
Save kaw2k/bf8373f1eaf00cf6692b3a4cab554ade to your computer and use it in GitHub Desktop.

Hey Fam 👋

I struggled with using typescript and ramda for a long time and figured I would brain dump a little. Maybe it will help some one else! Specifically, I struggled with how typescript can't always infer generics that ramda defines. The result is writing verbose types that are not really necessary, or forcing types with as and !.

Generics are super weird when getting into them, that coupled with ramda being a monster and typescript not being able to readily infer everything is a perfect shitstorm.

Whenever I work with ramda, my workflow is:

  1. Do a thing and assign it to a variable.
  2. If the variable isn't the type I expect, jump to the definition of the function in ramda
  3. Look for any generics the function may have and try to figure out what they do
  4. Fill in those generics where i call the function

Let's look at the groupBy in ramda.

/**
 * Splits a list into sublists stored in an object, based on the result of
 * calling a String-returning function
 * on each element, and grouping the results according to values returned.
 */
groupBy<T>(fn: (a: T) => string, list: T[]): { [index: string]: T[] };
groupBy<T>(fn: (a: T) => string): <T>(list: T[]) => { [index: string]: T[] };

Looks like groupBy has two definitions. My guess is because it is a curried function that takes two parameters, so there are two possible ways you can call it:

groupBy('one')({}) 
groupBy('one', {})

There is one generic T right next to the name of the function groupBy<T>. This is just saying "Hey look, I don't actually know what values are going to be used in this function, but here is some placeholder T you can use to write the function definition. When you actually use it, we will look at what you pass into that placeholder value and figure out if you done goofed or not".

Let's dissect the first type a bit

groupBy<T>(fn: (a: T) => string, list: T[]): { [index: string]: T[] };

We have two arguments. The first argument named fn looks like it is a function that takes in the placeholder value of T and returns a string (fn: (a: T) => string). The second argument named list is an array of whatever the placeholder value T is (list: T[]). Lastly, the return value is an object that can have any number of keys that are strings and the values are arrays of that placeholder ({ [index: string]: T[] }).

Now a few helpful things about this.

Uniformity Everywhere you see this placeholder T it has to be the same throughout the function. That means the argument passed to fn is the same type as the value of the list and the same type as the value of the return object's keys.

Inference Typescript (should) automatically infer this! For example:

type Person = { name: string, goofy: boolean }

const people: Person[] = [
    { name: 'rasa', goofy: true },
    { name: 'sam', goofy: false },
    { name: 'jay', goofy: true }
]

const fn = (person: Person) => person.name

groupBy(fn, people)

Typescript will look at the first argument which is a placeholder and see that it is of type (person: Person) => string. It looks at the original type groupBy<T>((fn: (a: T) => string)) and is able to figure out that the T in the original type is Person. It then fills in Person for all other places T exists in the function.

The result should be that typescript now knows the return value of this function call is { [key: string]: Person[] }.

Manual entry When Typescript fails to infer for whatever reason, we can help it out by manually filling in this generic value. Sometimes this is desireable as it forces us to actually do what we want vs doing something and making sure we are consistent.

We do this by adding in angle braces when we call the function and pass in the type we expect things to be.

groupBy<Person>(fn, people)

More Complex Generics We are not limited to only a single generic, and we can also specify that these placeholder values adhere to certain constraints. For example we could say groupBy<T extends Object>(). This signature forces the type of T to be an object of some shape!

Now there are other major pitfalls with using ramda and Typescript, looking at you compose. Typescript doesn't play too nicely with inferring curried functions, and then you slap on composing functions? All bets are off. When you go down that route, you really need to supply generic values. The way I do it is call each function individually, assign it to a value, make sure the value is right, and add it to compose 😭. Another reason I don't use compose too much nowadays.

Just remember generics is a way to write more ahem generic functions in statically typed languages. Instead of writing two join functions (one for lists and another for strings) we can have a single function that handles both cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment