7 code-style variations for strongly-typed JSON

Intro

I don't know about you, but I like my JSON data to get typed as soon as possible.

Whether I wrap it, assign it or destructure it, it's great to be able to see properties pop up as you type.

Whilst exploring ways to do this, I've been consistently surprised how flexible TypeScript's compiler can be – especially in regard to destructuring – so a while ago I started noting the various ways to do it.

Dataset

Let's say you have some contact data with top-level and nested properties:

const data = {
  "id": 1,
  "name": "joe",
  "contact": {
    "email": "joe@bloggs.com",
    "phone": "020 0000 0000"
  }
}
TypeScript

For the sake of momentum let's assume this data is safe; either it's hardcoded JSON or has been sanitised.

It would be good to get it fully typed, so:

  • TypeScript will complain if we try to do something we shouldn't
  • we can dot-into both top level and nested data
  • we can pass it around the app

Let's begin by defining the types:

type User = {
  id: number,
  name: string,
  contact: Contact
}

type Contact = {
  email: string,
  phone: string
}
TypeScript

The rest of the article will demonstrate variations in code-style to safely map our types to the data.

Code

The naive way would be to assign types manually:

function process (data: any) {
  const id: number = data.id
  const name: string = data.name
  const contact: any = data.contact
}
TypeScript

But we can do better than that, as we've already have some type information:

function process (data: any) {
  const id: number = data.id
  const name: string = data.name
  const contact: Contact = data.contact
}
TypeScript

If destructuring is your thing, you can do that too:

function process (data: any) {
  const { id, name, contact }: { id: number, name: string, contact: Contact } = data
}
TypeScript

But we're duplicating effort, as we could leverage our existing User type:

function process (data: any) {
  const { id, name, contact }: User = data
}
TypeScript

Moving the type to the parameter makes for one less step:

function process (user: User) {
  const { id, name, contact } = user
}
TypeScript

We can even destructure within the parameter!

function process ({ id, name, contact }: User) {
  // ...
}
TypeScript

If there's some reason you can't type the parameter, you can as to assert before you destructure:

function process (data: any) {
  const { id, name, contact } = data as User
}
TypeScript

If you're passing to a function that expects a User type, you can assert inline within the call:

process(data as User)
TypeScript

Conclusion

ES6 destructuring and TypeScript types make a great combination, and TypeScript's compiler is flexible enough that there's multiple ways to type values with a terse and expressive style.

I hope you learned something!