Strojopis:Ve skutečnosti neověřuje vaše typy.

Typový skript je hezká věc:Umožňuje vám definovat typy a zajistit, aby vaše třídy a funkce dodržovaly určitá očekávání. Nutí vás to přemýšlet o tom, jaká data do funkce vložíte a co z toho získáte. Pokud se zmýlíte a pokusíte se zavolat funkci, která očekává bodnutí s - řekněme - číslem, kompilátor vám dá vědět. Což je dobře.

Někdy to vede k mylné představě:Setkal jsem se s lidmi, kteří věřili, že strojopis zajistí, že typy jsou takové, jaké říkáte, že jste. Ale musím vám říct:Typescript to nedělá.

Proč? Typescript pracuje na úrovni kompilátoru, ne za běhu. Pokud se podíváte na to, jak vypadá kód, který Typescript vytváří, uvidíte, že se převádí do Javascriptu a odstraňuje všechny typy z kódu.

Kód strojopisu:

const justAFunction = (n: number): string => {
  return `${n}`
}

console.log(justAFunction)

Výsledný kód Javascript (za předpokladu, že převádíte na novější verzi EcmaScript):

"use strict";
const justAFunction = (n) => {
    return `${n}`;
};
console.log(justAFunction);

Pouze kontroluje, zda se typy zdají být správné na základě vašeho zdrojového kódu. Neověřuje skutečná data.

Typy kontrol

Je pak strojopis k ničemu? No, ne, zdaleka ne. Když jej používáte správně, nutí vás kontrolovat vaše typy, pokud neexistují žádné záruky ("bohužel" to také poskytuje několik jednoduchých způsobů, jak ven).

Změňme trochu náš příklad:

const justAFunction = (str: string[] | string): string => {
  return str.join(' ') 
}

console.log(justAFunction(["Hello", "World"]))
console.log(justAFunction("Hello World"))

Při kompilaci to povede k následující chybě:

index.ts:2:14 - error TS2339: Property 'join' does not exist on type 'string | string[]'.
  Property 'join' does not exist on type 'string'.

2   return str.join(' ')
               ~~~~


Found 1 error in index.ts:2

Překladač nutí přemýšlet o typu proměnné str . Jedním z řešení by bylo povolit pouze string[] do funkce. Druhým je otestovat, zda proměnná obsahuje správný typ.

const justAFunction = (str: string[] | string): string => {
  if (typeof str === 'string') {
    return str
  }

  return str.join(' ') 
}

console.log(justAFunction(["Hello", "World"]))
console.log(justAFunction("Hello World"))

To by se také převedlo do Javascriptu a typ by se otestoval. V tomto případě bychom měli pouze záruku, že se jedná o string a my bychom pouze předpokládali, že se jedná o pole.

V mnoha případech to stačí. Ale jakmile se musíme vypořádat s externím zdrojem dat - jako jsou API, soubory JSON, uživatelský vstup a podobně - neměli bychom předpokládat, že data jsou správná. Měli bychom ověřit data a je zde příležitost zajistit správné typy.

Mapování externích dat na vaše typy

Prvním krokem k vyřešení tohoto problému by tedy pravděpodobně bylo vytvoření skutečných typů, které by odrážely vaše data.

Předpokládejme, že API vrátí uživatelský záznam takto:

{
  "firstname": "John",
  "lastname": "Doe",
  "birthday": "1985-04-03"
}

Pak můžeme chtít vytvořit rozhraní pro tato data:

interface User {
  firstname: string
  lastname: string
  birthday: string
}

A pomocí načtení načtěte uživatelská data z API:

const retrieveUser = async (): Promise<User> => {
  const resp = await fetch('/user/me')
  return resp.json()
}

To by fungovalo a strojopis by rozpoznal typ uživatele. Ale může vám to lhát. Řekněme, že datum narození by obsahovalo číslo s časovým razítkem (to může být poněkud problematické pro lidi narozené před rokem 1970... ale o to teď nejde). Tento typ by stále považoval narozeniny za řetězec, přestože v něm bylo skutečné číslo... a Javascript je bude považovat za číslo. Protože, jak jsme řekli, Typescript nekontroluje skutečné hodnoty.

Co bychom teď měli dělat. Napište funkci validátoru. Může to vypadat nějak takto:

const validate = (obj: any): obj is User => {
  return obj !== null 
    && typeof obj === 'object'
    && 'firstname' in obj
    && 'lastname' in obj
    && 'birthday' in obj
    && typeof obj.firstname === 'string'
    && typeof obj.lastname === 'string'
    && typeof obj.birthday === 'string'
}

const user = await retrieveUser()

if (!validate(user)) {
  throw Error("User data is invalid")
}

Tímto způsobem se můžeme ujistit, že data jsou taková, jaká jsou. Ale můžete vidět, že se to ve složitějších případech může rychle vymknout z rukou.

Existují protokoly, které se přirozeně zabývají typy:gRPC, tRPC, validující JSON proti schématu a GraphQL (do určité míry). Ty jsou obvykle velmi specifické pro určitý případ použití. Možná budeme potřebovat obecnější přístup.

Zadejte Zod

Zod je chybějící článek mezi typy Typescriptu a vynucováním typů v Javascriptu. Umožňuje vám definovat schéma, odvodit typ a ověřit data jedním přetažením.

Naše User typ by byl definován takto:

import { z } from 'zod'

const User = z.object({
    firstname: z.string(),
    lastname: z.string(),
    birthday: z.string()
  })

Typ by pak mohl být extrahován (odvozen) z tohoto schématu.

const UserType = z.infer<User>

a validace vypadá takto

const userResp = await retrieveUser()
const user = User.parse(userResp)

Nyní máme typ a ověřená data a kód, který jsme museli napsat, je jen nepatrně větší než bez funkce ověření.

Závěr

Při práci s Typescriptem je důležité znát rozdíl mezi kontrolami kompilátoru a runtime validací. Abychom se ujistili, že externí data odpovídají našim typům, potřebujeme mít nějaké ověření. Zod je skvělý nástroj, jak se s tím vypořádat bez velkých nákladů a flexibilně.

Děkuji za přečtení.