How to write a bit safer types in TypeScript
Tags: typescript
Published: 2021-08-07 20:32:33

In this article you will learn:
- how to make your typescript functions even more safer
- some tricky differences between types and interfaces
- how to make types for your data structure more safe

**Part 1** - safer functions

Consider this example which is stolen from TypeScript docs:
```typescript

type Animal = { tag: 'animal' }

type Dog = Animal & { bark: true }
type Cat = Animal & { meow: true }

declare let animal: (x: Animal) => void;
declare let dog: (x: Dog) => void;
declare let cat: (x: Cat) => void;

animal = dog; // ok without strictFunctionTypes and error with

dog = animal; // should be ok

dog = cat; // should be error
```
Very simple code, nothing complicated.

`Animal` is a supertype for `Dog` and `Cat`.
There are a lot of typescript projects in the wild without `strict` flags. If you have active `strictFunctionTypes` flag, please disable it.

After disabling, you will see that `animal = dog` does not produces an error despite the fact that it isn’t `provably` sound.

*This is unsound because a caller might end up being given a function that takes a more specialized type, but invokes the function with a less specialized type. In practice, this sort of error is rare, and allowing this enables many common JavaScript patterns*

Of course if you have a big project you can't just turn on `strictFunctionTypes` and fix all errors.This is not always possible. So how to live without this flag?

Answer is simple. Just use generics.
```typescript
type Animal = { tag: 'animal' }
type Dog = Animal & { bark: true }

// generic is here
declare let animal: <T extends Animal>(x: T) => void;
declare let dog: (x: Dog) => void;

animal = dog; // error even without strictFunctionTypes
```
Almost forgot, prefer arrow function notation inside interfaces rather than method notation:
```typescript
// unsafe
interface Bivariant<T> {
  call(x: T): void
}

// safe
interface Contravariant<T> {
  call: (x: T) => void
}
```

**Part 2** - tricky differences between types and interfaces

Imagine you have untyped `handleRecord` function:
```typescript
interface Animal {
  tag: 'animal',
  name: 'some animal'
}

declare var animal: Animal;

const handleRecord = (obj:any) => { }

const result = handleRecord(animal)
```
You know that this function expects and object. You can replace `any` with `object` type, but eslint will not be happy about this change and will suggest you to use `Record<string, unknown>` instead.
```typescript
interface Animal {
  tag: 'animal',
  name: 'some animal'
}

declare var animal: Animal;

const handleRecord = (obj:Record<string, unknown>) => { }

const result = handleRecord(animal) // error
```
As you might have noticed, it still does not work. Because `interfaces` are not indexed by the default and we still can't pass `Animal` object to `handleRecord`. So, what we can do to fix our type and don't break dependent types?

Just use `type` instead of `interface`.
```typescript

type Animal= {
  tag: 'animal',
  name: 'some animal'
}

declare var animal: Animal;

const handleRecord = (obj:Record<string, unknown>) => { }

const result = handleRecord(animal) // ok
```

**Part 3** - safer data structure
Consider this example:
```typescript
interface Animals {
  dog: 'Sharky',
  cat: 'Meout'
}

type AnimalEvent<T extends keyof Animals> = {
  name: T
  call: (name: Animals[T]) => void
}
```

Seems that `AnimalEvent` constructor type is perfectly fine. Yea, why not? Let's use it as a function argument or array element:
```typescript
const handleEvent = <T extends keyof Animals>(event: AnimalEvent<T>) => { }

// we would expect an error but it compiles 
const arrayOfEvents: AnimalEvent<keyof Animals>[] = [{
  name: 'dog',
  call: (name: 'Meout') => { }
}]

// should be error but it compiles
handleEvent<keyof Animals>({
  name: 'dog',
  call: (name: 'Meout') => { }
})
```
It is defenitely something wrong with our type because it allows us to represent invalid state. We all know that invalid state should not be representable if we use TypeScript.

Let's refactor it a bit:
```typescript
interface Animals {
  dog: 'Sharky',
  cat: 'Meout'
}

type EventConstructor<T extends keyof Animals> = {
  name: T
  call: (name: Animals[T]) => void
}
/**
 * Retrieves a union of all possible values
 */
type Values<T> = T[keyof T]

// "Sharky" | "Meout"
type Test = Values<Animals>

// EventConstructor<"dog"> | EventConstructor<"cat">
type AnimalEvent = Values<{
  [Prop in keyof Animals]: EventConstructor<Prop>
}>

const handleEvent = (event: AnimalEvent) => { }

// error
const arrayOfEvents: AnimalEvent[] = [{
  name: 'dog',
  call: (name: 'Meout') => { }
}]

// error
handleEvent({
  name: 'dog',
  call: (name: 'Meout') => { }
})
```
Instead of using generic for animal name we have created a union of all possible `AnimalEvents` representation. Hence - illegal state is unrepresentable.

These techniques are easy to use and simple to understand.