Typescript & NodeJS: Collection of 16 TILs

I've been working on a project that is based on Typescript, NodeJS, and AWS Lambda. Typescript is new to me, so I have to learn a lot. For the learning, I like the idea of Josh Branchaud for capturing learning in small Today-I-Learned (TIL) nuggets. So I opened this blog post and I'll keep adding TILs as I go, newest at the top.

arrays support some and every

This one is Javascript-related, but it turns out that arrays support some and every. Both functions take a predicate and they test if the predicate is true for some or every element of the array, respectively. So there's no need to combine filter with length to achieve the same. More on arrays functions in the Javascript docs.

const isEven = (f: number) => f % 2 === 0

// test if an array contains an even number
console.log([1, 2, 3].some(isEven)) // prints true
console.log([1, 3, 5].some(isEven)) // prints false

// test if an array consists only of even numbers
console.log([1, 2, 3].every(isEven)) // prints false
console.log([2, 4, 6].every(isEven)) // prints true

function currying with static type checks

I needed to curry a function in Typescript to simplify code, but with the static type checks in place. I explored currying from Rambda, Lodash and also the vanilla fat-arrow syntax. Both the Lodash and the fat-arrow approach ensured proper types, Rambda didn't not:

interface Foo {}
interface Bar {}
type Trigger = "created" | "updated"
const foos = [] as Foo[]

// function we'd like to curry...
const fooToBar = (trigger: Trigger, foo: Foo): Bar => ({})
// ...to simplify this map
const bars = foos.map((foo) => fooToBar("created", foo))

// Rambda
import R from "rambda"
const fooToBarCurriedRambda = R.curry(fooToBar)
// 😥 this compiles, but should not: number is not Foo
foos.map(fooToBarCurriedRambda(1))

// Lodash
import { curry } from "lodash"
const fooToBarCurriedLodash = curry(fooToBar)
// 🎉 compilation error: number is not assignable to Foo
foos.map(fooToBarCurriedLodash(1))
// 🎉 compilation error: Bar[] not assignable to number[]
const foosLodash: number[] = foos.map(fooToBarCurriedLodash("created"))

// vanilla Typescript with fat arrows
const fooToBarCurriedFatArrow = (trigger: Trigger) => (foo: Foo): Bar => ({})
// 🎉 compilation error: number is not assignable to Foo
foos.map(fooToBarCurriedFatArrow(1))
// 🎉 compilation error: Bar[] not assignable to number[]
const foosFatArrow: number[] = foos.map(fooToBarCurriedFatArrow("created"))

For now, I went with the fat arrow approach. I've also stumbled upon the bind approach that I haven't explored yet.

reusing index in indexable types

In indexable types, we can reuse the index for value types as well. This is very handy. For example, below we have interfaces Foo and Bar. Type BarToFooMap defines mapping from Bar to Foo. With indexable types and keyof we can ensure the mapped values are of correct type:

interface Foo {
  a: string
  b: number
}

interface Bar {}

type BarToFooMap = {
  [k in keyof Foo]: (bar: Bar) => Foo[k] // <-- 🎉 using index type k in value type
}

const barToFooBroken: BarToFooMap = {
  a: () => "hello",
  b: () => "world", // 🎉 compilation error: improper return type for b
}

const barToFooFixed: BarToFooMap = {
  a: () => "hello",
  b: () => 1, // this compiles 🎉
}

checking against a string literal type at runtime

String literal types are not available at runtime, so how if one needs to check against them at runtime? It turns out this is possible with const assertions:

const Fruits = ["Apple", "Banana"] as const
type Fruit = typeof Fruits[number]
const isFruit = (s: String) => Fruits.includes(s as Fruit)

Deeper dive here.

const assertions are super handy

It turns out that const assertions are super handy to simplify typing. Code below uses const assertion on an object literal and, in fact, it compiles:

const s = {
  1: "foo",
  2: "bar",
} as const

function fn(s: "foo" | "bar") {}

fn(s[1])

Without the assertion, the type of s[1] is string, and string is not assignable to 'foo' | 'bar'. Deeper dive here.

absolute imports in WebStorm

WebStorm can be configured to import modules using absolute paths instead of relative paths:

webstorm-import-absolute

It works as follows:

// if the field unchecked, WebStorm imports relative
import { queries } from "../crm/queries"

// if the field checked, WebStorm imports absolute
import { queries } from "crm/queries"

mocking with jest-when

There is a handy library for mocking function return values, called jest-when:

when(fn)
  .calledWith(1)
  .mockReturnValue("yay!")
  .calledWith(2)
  .mockReturnValue("nay!")

Sidenote on mocks: After seeing Simple Made Easy talk by my engineering guru Rich Hickey, I realized that mocks most probably signal complecting of two unrelated components and thus point to accidental complexity. To remove the complexity, one can pass mocked data via function arguments instead.

empty arrays are not falsy

It turns out that in Javascript empty arrays are not falsy:

const xs = [] as number[]
if (!xs) {
  console.log("I'm empty")
}
// doesn't print 'I'm empty'

To test that an array is empty in Typescript, you need to check the length:

const xs = [] as number[]
if (!xs.length) {
  console.log("I'm empty")
}
// does print 'I'm empty'

keeping types dry

It turns out that with typeof it is possible to keep types dry, for example:

const fn = async (apples: Apples[]) => {
  const shinyApples: typeof apples = []
  //...
}

Sidenote on array mutability: Mutability complects state and time and is thus a form of complexity. Immutable values are simpler and lead to lower accidental complexity.

utility types

Typescript supports utility types that allow to transform a type into another type. For example:

  • Set all properties of Todo to optional with Partial<Todo>
  • Omit a property id from Todo with Omit<Todo, 'id'>
  • Pick a set of properties from Todo with Pick<Todo, 'id' | 'name'>

null vs undefined

There's a difference between null and undefined. undefined means a variable has been declared but has not yet been assigned a value. null is an assignment value; it can be assigned to a variable as a representation of no value. So I should use null to represent no value, such as

interface ApiReponse {
  createdAt: string | null
}

This picture explains the concept visually.

jest watch mode

It turns out that jest CLI has a watch mode: tests are rerun automatically on code change. That's handy, especially in combination with test selection.

npx jest --watch src/foo/__tests__/bar.test.ts

prettier: automatic code formatter

Since I discovered prettier, I cannot imagine how I could have worked without it. Prettier automatically formats code, such as Javascript, Typescript, JSON, YAML and Markdown. It works across IDEs and very well within a team.

By far the biggest reason for adopting Prettier is to stop all the on-going debates over styles. It is generally accepted that having a common style guide is valuable for a project and team but getting there is a very painful and unrewarding process. People get very emotional around particular ways of writing code and nobody likes spending time writing and receiving nits. – Prettier docs (section Why Prettier)

optional chaining

There's a handy syntactic sugar introduced in Typescript 3.7 called optional chaining.

# without optional chaining
const baz = foo && foo.bar  && foo.bar.baz

# with optional chaining
const baz = for?.bar?.baz

The ?. operator functions similarly to the . chaining operator, except that instead of causing an error if a reference is nullish (null or undefined), the expression short-circuits with a return value of undefined.

MDN Web docs

It works also for arrays.

some handy libraries

controlling NodeJS versions with nvm

NodeJS comes with different versions. Each version bundles the npm as well. For example, NodeJS v15 comes with NPM v7, while NodeJS v14 comes with NPM v6. Here's an overview.

When working on multiple projects, I've found it handy to switch between multiple NodeJS versions. That's when nvm comes into play. nvm also supports .nvmrc to further automate the switching.


Would you like to connect? Subscribe via email or RSS , or follow me on Twitter!


Post changelog

2021-04-06Added some and every
2021-04-02Added function currying with static type checks
2021-04-01Added reusing index in indexable types
2021-03-30Added absolute imports in Webstorm
2021-03-30Added const assertions
2021-03-30Added checking string literal types at runtime"
2021-03-29post first published