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 😉.

A red cardinal bird sitting in a tree.

Foto frå Timothy DykesUnsplash.