Most APIs which I work with return JSON objects, and I find it useful parsing these into custom classes in Typescript rather than manipulating the JSON. Read on to understand why this is helpful, and how to do it.
Assuming your application receives some JSON response from a server, eg
/*
// The raw JSON response
animals = [
{
metadata: {
tags: ["paws", "fur"]
},
fields: {
name: "Santa Paws",
type: "puppy",
born: "2021-05-13",
vaccinated: "2021-05-18",
},
},
]
*/
const [animals] = await Promise.all([
client.getAnimals({
order: '-born',
}),
])
Now let’s say you want to parse this JSON response as a custom class so that you can use it consistently throughout your Vue app, with some useful helper functions. The custom class might look like this:
// animal.ts
// Your app's custom class
class Animal {
name: string
type: string
born: Date
tags: string[]
constructor (name: string, type: string, born: string, tags: string[]) {
this.name = name
this.type = type
// convert the string from the JSON response to a Date object
this.born = Intl.DateTimeFormat("en-GB", {dateStyle: 'medium'}).format(new Date(born))
this.tags = tags
}
// Add utility functions for your custom class
greet() {
return `Hello, my name is ${ this.name } and I'm a ${ this.type }!`
}
}
interface IResponse {
metadata: {
tags: string[],
},
fields: {
name: string,
type: string,
born: string,
// if you don't need the `vaccinated` field, just don't include it in your interface and it will be ignored
},
}
module.exports = {
Animal,
parseAsAnimal(response: IResponse): Animal {
return new Animal(
response.fields.name,
response.fields.type,
response.fields.born,
response.metadata.tags,
)
},
}
You may have noticed some benefits to transforming the API response into a custom class already, namely:
you get to use typing / type inference, rather than just using a simple JSON object of unknown type and structure
you can simplify the data structure (eg collapsing `response.fields.name` to just `Animal.name`)
you can add utility functions to your custom class, like `greet()` in this example
Handy hint: regardless of the structure of the JSON object, you can declare an interface for only the fields which you need and everything else gets ignored.
So how might you use this custom `Animal` class in your Vue app?
Here's a simple implementation:
// index.vue
<template>
<Detail
v-for="(animal, index) in animals"
:key="index"
:animal="animal"
/>
</template>
<script>
import { parseAsAnimal } from "animal.ts"
export default {
async asyncData() {
const [responses] = await Promise.all([
client.getAnimals({
order: '-born',
}),
])
const animals = responses.map(
response => parseAsAnimal(response)
)
return {
animals
}
}
}
</script>
// detail.vue
<template>
<h1>{{ animal.name }}</h1>
</template>
<script lang="ts">
import { PropType } from "vue"
import { Animal } from "animal"
export default {
props: {
animal: {
type: Object as PropType,
}
}
}
</script>
Let's break this down a little.
The code in `index.vue` receives the API response and sends it to the `parseAsAnimal()` function, where new `Animal` instances are created for each JSON object.
This is then used in the template's `v-for` to create a `Detail` component for every animal.
You'll notice the use of `PropType` in `detail.vue`. This is Vue's support for Typescript by annotating props - if you didn't do this then the `animal` prop would be an `Object` rather than the custom `Animal` class. The declaration of `<script lang="ts">` is therefore necessary on this component.