DTO - A typescript utility type
Tags: typescript
Published: 2021-02-17 09:23:19

`Dto` (Data Transfer Object) is a TypeScript utility type that can be used to represent an object that is going "over the wire". It's meant to be used at the boundaries of an application. You can find the code at https://github.com/tamj0rd2/dto and at the bottom of this post.

## What problem does it try to solve?

In Javascript, sending objects via network requests is generally pretty easy, but a problem arises when making use of classes. Take the following example using a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date)

Here's some client side code that tries to create a Post via the endpoint `/api/posts`:

```typescript
interface Post {
  author: string
  title: string
  datePublished: Date
}

const post: Post = {
  author: 'tamj0rd2',
  title: 'Something about TypeScript',
  datePublished: new Date()
}

await fetch('/api/posts', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify(post)
})
```

Let's take a look at how we're handling this request on the server side:

```typescript
import express from 'express'

const app = express()
app.use(express.json()) // parses our request body into JSON

app.post<unknown, unknown, Post>('/api/posts', (req, res) => {
  // if you were to hover your mouse over datePublished below, your IDE would say its type is Date
  console.log(`The type of datePublished is '${typeof req.body.datePublished}'`)
  res.writeHead(201).end()
})

app.use((_, res) => res.sendStatus(404))
app.listen(8080, () => console.log('server listening on http://localhost:8080'))
```

If you thought the output of that console log line might say the type of datePublished is `object`, you'd be wrong. The actual type is `string`. Remember, the post was stringified on the client side and then parsed back into an object on the server side. In Javascript, the stringified result of a `Date` is a `string` and the parsed result of a `string` is... a `string`.

`Date` isn't the only class whose stringified structure is different to what you might expect. `Set` and `Map` don't stringify in a way you'd expect at all.

```typescript
JSON.stringify(new Set([1, 2, 3])) // outputs "{}" but what I actually want is "[1, 2, 3]"

const myMap = new Map<string, string>()
myMap.set('Hello', 'World')
JSON.stringify(myMap) // outputs "{}" but what I actually want is "{"Hello":"World"}"
```

So although we've told our request handler that the request body should be of type `Post`, it isn't actually true. This would be more accurate:

```typescript
interface PostDto {
  author: string
  title: string
  datePublished: string
}

app.post<unknown, unknown, PostDto>('/api/posts', (req, res) => {
  console.log(`The type of datePublished is '${typeof req.body.datePublished}'`)
  res.writeHead(201).end()
})
```

Why do I care about the accuracy of these types? They've helped to catch bugs in multiple codebases I've contributed to. One common case is trying to call functions on something you think is a Date but is actually a string. I like these types to be accurate for the same reason that I like TypeScript. It helps me catch bugs sooner rather than later.

#### But I don't want to write and maintain two different versions of an interface >:(

Well that's what the `Dto` utility type attempts to solve! The examples I'm showing in this post are so small that you might be wondering why I'm bothered by it. I've faced the pain of rewriting interfaces with dozens of properties and/or nested objects. It's time consuming and I don't like doing it. Once you have two interfaces you also have to maintain them and keep them in sync whenever you add, remove or change the types of properties.

What the `Dto` utility type doesn't do is serialize or validate your data. It's just a `type`. It can't do serialization or validation for you because it doesn't exist at runtime. You're still responsible for getting your code from it's "initial" type to it's "Dto-ified" type.

## Example usage

```typescript
app.post<unknown, unknown, Dto<Post>>('/api/posts', (req, res) => {
  res.writeHead(201).end()
})
```

In this example, instead of having to write out another interface for our "over the wire" version of a Post, we're just using the `Dto` utility type. `Dto<Post>` is equivalent to the `PostDto` interface we wrote out earlier.

## Show me the money

Here it is! Although the most up to date version can always be found [here](https://github.com/tamj0rd2/dto/blob/master/src/dto.ts)

```typescript
type IsOptional<T> = Extract<T, undefined> extends never ? false : true
export type Func = (...args: any[]) => any
type IsFunction<T> = T extends Func ? true : false
type IsValueType<T> = T extends
  | string
  | number
  | boolean
  | null
  | undefined
  | Func
  | Set<any>
  | Map<any, any>
  | Date
  | Array<any>
  ? true
  : false

type ReplaceDate<T> = T extends Date ? string : T
type ReplaceSet<T> = T extends Set<infer X> ? X[] : T
type ReplaceMap<T> = T extends Map<infer K, infer I>
  ? Record<
      K extends string | number | symbol ? K : string,
      IsValueType<I> extends true ? I : { [K in keyof ExcludeFuncsFromObj<I>]: Dto<I[K]> }
    >
  : T
type ReplaceArray<T> = T extends Array<infer X> ? Dto<X>[] : T

type ExcludeFuncsFromObj<T> = Pick<T, { [K in keyof T]: IsFunction<T[K]> extends true ? never : K }[keyof T]>

type Dtoified<T> = IsValueType<T> extends true
  ? ReplaceDate<ReplaceMap<ReplaceSet<ReplaceArray<T>>>>
  : { [K in keyof ExcludeFuncsFromObj<T>]: Dto<T[K]> }

export type Dto<T> = IsFunction<T> extends true
  ? never
  : IsOptional<T> extends true
  ? Dtoified<Exclude<T, undefined>> | null
  : Dtoified<T>

export type Serializable<T> = T & { serialize(): Dto<T> }
```

<hr/>

Since I wrote this I've learned more about Typescript and there is definitely some room for improvement, but hopefully someone else will get some use out of this. I'm planning to follow this up with another post on how I wrote the utility type and how I made sure I didn't break it as I added more functionality. Types are fiddly!

In the meantime, you can learn more about a lot of the tools that went into creating this type using the [TypeScript utility types documentation](https://www.typescriptlang.org/docs/handbook/utility-types.html) and this blog post on [Conditional types](https://artsy.github.io/blog/2018/11/21/conditional-types-in-typescript/). I can't recommend that post enough.