Min fyrste rekursive type i Typescript
Eg må sei at eg har prøvd å unngå å hamne på Typescript-kjøret og overbruke alle finessene i dette språket.
God kode er kode som er lett å vedlikehalde og videreutvikle. Både kollegaene mine og eg må kunne forstå koden, både no og i framtiden. Og med dette atterhaldet herved atterhaldt, så vil eg gjerne dele denne kodesnutten som er eit døme på ein rekursiv, betinget, utledet type.
Hold på hatten og les i vei.
/**
* An example API response where all fields are expressed
* as strings.
*/
const rawFishData = {
id: "321",
name: "Selmer Salmon",
type: "wild salmon",
details: {
weight: "5.3",
amountOfBlubb: "9001"
},
tracking: [{
lat: "60.408773",
lon: "5.287155",
dateTime: '2020-03-23T19:27:20.942Z'
}]
}
/**
* In our program we want to parse the raw data into
* this more manageable data structure. Instead of
* wondering if we had remembered to convert the
* dateTime properties to proper dates we should
* enforce that the type has a proper date type.
*/
interface FishData {
id: number
name: string
type: string
details: {
weight: number
amountOfBlubb: number
}
tracking: {
lat: number
lon: number
dateTime: Date
}[]
}
/**
* This conditional type will convert any type into a
* type containing only strings.
*/
type Stringify<T> = {
[K in keyof T]:
// if it's an array, infer the contained type and recurse.
T[K] extends (infer U)[] ? Stringify<U>[]
// any dates will be described as strings from the API.
: T[K] extends Date
? string
// we'll need to traverse into any objects and process them
: T[K] extends object
? Stringify<T[K]>
// anything else will be converted to a string
: string
}
/**
* With the Stringify<T> type we can now use our target type that the want
* and express the rawApiResponse which is a FishData object containing only
* strings. By enforcing these strict input and output types TS will help us
* ensure that we in fact convert all necessary fields.
*/
function parseApiResponse(rawApiResponse: Stringify<FishData>): FishData {
const { id, details: { weight, amountOfBlubb }, tracking } = rawApiResponse
return {
...rawApiResponse,
id: Number.parseInt(id),
details: {
weight: Number.parseFloat(weight),
amountOfBlubb: Number.parseInt(amountOfBlubb)
},
tracking: tracking.map(({ lat, lon, dateTime }) => ({
lat: Number.parseFloat(lat),
lon: Number.parseFloat(lon),
dateTime: new Date(dateTime)
}))
}
}
const parsedFishData = parseApiResponse(rawFishData)
// Result
// {
// "id": 321,
// "name": "Selmer Salmon",
// "type": "wild salmon",
// "details": {
// "weight": 5.3,
// "amountOfBlubb": 9001
// },
// "tracking": [
// {
// "lat": 60.408773,
// "lon": 5.287155,
// "dateTime": "2020-03-23T19:27:20.942Z"
// }
// ]
// }
Køyr koden i TS sin online lekegrind.
Eg tykkjer Stringify<T>
funksjonen er ganske bananas. Men eg kjem definitivt til å bruke den i eit reelt prosjekt der me henter data i frå eit API der alle felta er satt til å vere tekst, og så må koden vår passe på å konvertere alle relevante felt til riktige datatyper.
Til no har me duplisert type beskrivinger som EksempelType.d.ts
og lagd typer som RawEksempelType.d.ts
med alle felta satt til string
. Resultatet er at kvar gong me vil endre på EksempelType.d.ts
så må me også hugse på å oppdatere Raw versjonen. Og siden dette er til dels store, nøstede typer så var det ekstra kjekt å kunne kome fram til dette alternativet der me får bygd Raw versjonen basert på den originale typen.
Eg vil runde av med å sei at Typescript er bra greier om ein bruker det i tilmålte mengder uten å overvelde seg sjølv og kollegaene sine. Eg merker eg må passa på å ha ein intern tidsavgrensning på kor lengje eg lyt freiste å lage den perfekte datatypen. Om det dreg ut å kode ferdig ein gitt funksjonalitet så er det betre å velje den nest beste type beskrivingen og kanskje leggje igjen ein hugselapp i koden. Då får ein tatt utbytte av typetryggleiken uten sprengje prosjektbudsjettet 😉.