Upotreba rekurzivnih tipova aliasa u Typescriptu


Imam neki kod u kojem želim pokušati ukloniti upotrebu:

let result: any;

rezultat može biti veliki JSON vraćen s REST API poziva, i zamijeniti ga rekurzivnim alias tipom. Dakle, imam ovo:

type Json = string | number | boolean | null | Array<Json> | { [key: string]: Json };

Čim to učinim, moj kod se tada žali ovako:

if (result.reasonCode != "8700" && result.reasonCode != "8207") {
.
.}

zbog toga što reasonCode nije definiran. Dakle, tada sam pokušao dodati ovo - nisam siguran je li ovo ispravno ili nije. To je bio samo moj pokušaj da zaobiđem početnu grešku. Nadao sam se da neću morati koristiti sučelje. Koristim TypeScript V5.

interface JsonArray extends Array<Json> {
  reasonCode: string;
  message: string;
}

onda sam promijenio rezultat da bude:

let result: JsonArray;

sada ne dobivam nikakve greške pri kompajliranju za result.reasonCode.
ali sada dobivam novu grešku

kada ja napravim:

if (result === "error" {
.
.} 

s pogreškom da Čini se da je ovo uspoređivanje nenamjerno jer tipovi 'JsonArray' i 'string' nemaju preklapanja

da bih to zaobišao, tada sam morao prijeći na ovo:

let result: JsonArray | "error"; 

Nemam kontrolu nad REST API pozivom što se tiče onoga što vraća.

Sve ovo izgleda malo nespretno, pa tražim neki savjet.

Odgovor #1

Vaša rekurzivna alias tipa definira generičku JSON strukturu, bez specifičnih poznatih polja, pa je TypeScript u pravu kada vam govori da polje reasonCode možda ne postoji u vrijednosti ovog tipa.

Na primjer, u slučaju da je result === "error" istinito (što, prema vašem opisu, predstavlja moguću situaciju), tada je result tipa string i naravno nema polje reasonCode.

Ako bi vaš kod prvo provjerio da vrijednost s kojom radimo zaista nije broj, niz, boolean, null ili niz, tada bi TS mogao pretpostaviti da je vrijednost objekt par-ključ prema { [key: string]: Json }, što bi trebalo omogućiti pristup svim poljima pomoću string ključa.

Provjera toga je malo komplicirana, jer je typeof i za nizove i za null vrijednosti također "object". Trebalo bi biti nešto poput ovoga:

if (result && !Array.isArray(result) && typeof result === 'object')

Odgovor #2

Parsiranje JSON stringa obično je točka u kojoj neispravan kod može ući u vaš kod. Na primjer, ako je taj JSON string dohvaćen iz LocalStorage-a, moguće je da ga je korisnik izravno izmijenio. Bez odgovarajuće provjere tipova, to može dovesti do neobrađenih pogrešaka.

Postoji nekoliko načina za rješavanje ovog potencijalnog rizika, a koji će najbolje funkcionirati ovisit će o slučaju do slučaja.

Na primjer, ako možete biti vrlo sigurni da znate kako će izgledati vaš parsirani JSON objekt, mogli biste razmotriti korištenje tvrdnje tipa kako biste TypeScript kompajleru rekli koju vrstu objekta je vratio JSON.parse. Ovo vam neće pružiti nikakvu sigurnost tipa - vi ćete tvrdnjom potvrditi da je tip ispravan - ali ima prednost jer ne zahtijeva puno koda i ima vrlo nizak trošak održavanja.

Alternativno, možete taj tip pretvoriti u unknown umjesto da koristite zadano ponašanje TypeScripta koje vraća any. S ovim pristupom, TypeScript će od vas zahtijevati da suzite tip prije nego što ga koristite.


Kao kratka napomena, postoji npm paket pod nazivom ts-reset koji mijenja nekoliko ugrađenih tipova u TypeScriptu, a jedna od izmjena koju pravi je da JSON.parse vraća unknown radi poboljšane sigurnosti tipa.


Postoji nekoliko načina za upravljanje ovom vrstom provjere tipa. Još jednom, najbolji za vas ovisit će o vašem slučaju upotrebe.

Najočitiji pristup je da sami napišete provjeru tipa. To može dobro funkcionirati s vrlo jednostavnim tipovima, ali dolazi s određenim rizikom i troškom održavanja. Na primjer:

type Result = 'error' | {
    reasonCode: string;
    message: string;
}

function isResult(value: unknown): value is Result {
    if (value === 'error') {
        return true;
    }

    if (!(
        typeof value === 'object' &&
        value !== null
    )) {
        return false;
    }

    if (!(
        'reasonCode' in value &&
        typeof value.reasonCode === 'string'
    )) {
        return false;
    }

    if (!(
        'message' in value &&
        typeof value.message === 'string'
    )) {
        return false;
    }

    return true;
}

// ...

declare const resultJSON: string;

const result = JSON.parse(resultJSON) as unknown;

if (!isResult(result)) {
    throw new Error('Handle this error somehow');
}

result; // <- Result

TypeScript Igralište

Kao što sigurno možete primijetiti, složenost te funkcije prilagođenog zaštitnika tipova brzo će rasti kada se koriste složeniji tipovi. Također je moguće pogriješiti u provjerama koje radite, ali TypeScript vjeruje vašoj tvrdnji kada je u pitanju value is Result dio definicije tipa vaše funkcije.

Drugi pristup koji biste mogli primijeniti na ovaj problem, a koji bolje funkcionira s složenijim tipovima ili tipovima koji bi još uvijek mogli biti u promjeni (što se često događa tijekom razvoja), jest korištenje biblioteke koja vam omogućuje definiranje sheme tipa koja se može koristiti za generiranje i TypeScript tipova i funkcije za provjeru tipa koja se može pokrenuti u JavaScriptu.

Postoji nekoliko dobro uspostavljenih biblioteka koje popunjavaju ovu nišu. Osobno mi se prilično sviđa Zod, ali evo nekoliko drugih:

  • Ajv
  • Joi
  • myzod
  • Da

Evo primjera kako biste mogli koristiti Zod:

import { z } from 'zod';

const resultSchema =
    z.literal('error')
    .or(z.object({
        reasonCode: z.string(),
        message: z.string(),
    }));

// ...

declare const resultJSON: string;

const parsedResult = JSON.parse(resultJSON) as unknown;

// Will throw an error if `parsedResult` doesn't match the schema
// Otherwise, returns a deep copy with a well-defined type
const result = resultSchema.parse(parsedResult);

result; // <- 'error' | { reasonCode: string; message: string; }

TypeScript Igralište

Kao što možete vidjeti, ovo zahtijeva manje koda nego pisanje vlastite funkcije za provjeru tipa, ali također uključuje korištenje drugačije (i možda manje poznate) sintakse za definiranje vašeg tipa.

Postoje različiti troškovi i koristi za svaki od ovih pristupa, tako da ne mogu sa sigurnošću reći koji je najbolji za vas. Ali nadam se da sam vam barem dao neke korisne opcije.

Povezani članci