I Typescript, hvordan importere json og dynamisk slå opp etter nøkkel?


I TypeScript 3.0.3 importerer jeg en JSON-fil slik:

import postalCodes from '../PostalCodes.json';

Den har dette formatet:

{
    "555": { "code": 555, "city": "Scanning", "isPoBox": true },
    "800": { "code": 800, "city": "Høje Taastrup", "isPoBox": true },
    "877": { "code": 877, "city": "København C", "isPoBox": true },
    "892": { "code": 892, "city": "Sjælland USF P", "isPoBox": true },
    "893": { "code": 893, "city": "Sjælland USF B", "isPoBox": true },
    "897": { "code": 897, "city": "eBrevsprækken", "isPoBox": true },
    "899": { "code": 899, "city": "Kommuneservice", "isPoBox": true },
    "900": { "code": 900, "city": "København C", "isPoBox": true },
    "910": { "code": 910, "city": "København C", "isPoBox": true },
    "917": { "code": 917, "city": "Københavns Pakkecenter", "isPoBox": true },

... and so on

Jeg vil bruke det slik:

// first.postalCode is of type string
const x = postalCodes[first.postalCode];

Jeg får feilen: "Elementet har implisitt typen 'any' fordi typen '...veldig lang type signatur...' ikke har noen indeks-signatur."

Finnes det en måte å få dette til å fungere med de automatisk genererte json-typene, slik at jeg kan slå opp et postnummer ved hjelp av dets strengnøkkel dynamisk?

Min beste tilnærming akkurat nå er å ha en mellomliggende ts-fil som:

import postalCodes from './PostalCodes.json';

export const PostalCodesLookup = postalCodes as {
    [key: string]: { code: number, city: string, isPoBox: boolean }
};

Svar #1

Siden TypeScript v2.9 kan du aktivere resolveJsonModule-flagget i compilerOptions i din tsconfig.json-fil slik:

{
  "compilerOptions": {
    // ... other options
    "resolveJsonModule": true
  },
}

Nå bør TypeScript automatisk løse typene i din importerte JSON-fil.

For å omgå problemet med indekstype, kan jeg foreslå to alternativer:

  1. Aktiver suppressImplicitAnyIndexErrors i din tsconfig.json. Dette vil undertrykke denne feilmeldingen. Du vil heller ikke få noen typeindikasjoner.

  2. Lag noen typer for JSON, og bruk dem i stedet for bare string:

    import codes from '../codes.json';
    type PostalCode = keyof typeof codes;
    
    const goodSring: string = '555';
    const badString: string = '2';
    const goodCode: PostalCode = '555';
    const badCode: PostalCode = '2'; // Error:(39, 7) TS2322: Type '"2"' is not assignable to type '"555" | "800" | "877" | "892" | "893" | "897" | "899" | "900" | "910" | "917"'.
    const array: [] = [];
    const obj = {some: 'prop'};
    const num: number = 123;
    
    const list: PostalCode[] = [
        '555',
        '2', // Error:(43, 5) TS2322: Type '"2"' is not assignable to type '"555" | "800" | "877" | "892" | "893" | "897" | "899" | "900" | "910" | "917"'.
        goodCode,
        badCode,
        goodSring, // Error:(46, 5) TS2322: Type 'string' is not assignable to type '"555" | "800" | "877" | "892" | "893" | "897" | "899" | "900" | "910" | "917"'.
        badString, // Error:(47, 5) TS2322: Type 'string' is not assignable to type '"555" | "800" | "877" | "892" | "893" | "897" | "899" | "900" | "910" | "917"'.
        goodSring as PostalCode,
        badString as PostalCode, // no protection here
    
        array as PostalCode, // Error:(54, 13) TS2352: Conversion of type '[]' to type '"555" | "800" | "877" | "892" | "893" | "897" | "899" | "900" | "910" | "917"' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type '[]' is not comparable to type '"917"'.
        num as PostalCode, // Error:(55, 13) TS2352: Conversion of type 'number' to type '"555" | "800" | "877" | "892" | "893" | "897" | "899" | "900" | "910" | "917"' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
        obj as PostalCode, // Error:(56, 13) TS2352: Conversion of type '{ some: string; }' to type '"555" | "800" | "877" | "892" | "893" | "897" | "899" | "900" | "910" | "917"' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type '{ some: string; }' is not comparable to type '"917"'.
    ];
    

    Avhengig av hvor «hardkodet» bruken vil være, kan det å eksportere PostalCode-typen fungere bra for deg.

    Du kan også skrive en funksjon som sjekker mot JSON på kjøretid:

    import codes from '../codes.json';
    export type PostalCode = keyof typeof codes;
    
    function verifyCode(s: string): s is PostalCode {
        return codes[s as PostalCode] !== undefined; // or use Object.keys, or some other method
    }
    
    let city: string;
    const str: string = 'asd';
    
    if (verifyCode(str)) {
        city = codes[str].city; // in this branch `str` is `PostalCode`
    } else {
        city = codes[str].city; // complains about the index signature
    }
    

Svar #2

Jeg tror hovedproblemet ditt her ikke er at kompilatoren trenger å utlede typen til postalCodes på en annen måte, men at first.postalCode ikke er kjent for å være en av nøklene i postalCodes. Siden first.postalCode er av typen string, er det rimelig at kompilatoren advarer deg om dette.

Så du trenger å gjøre en slags typebeskyttelse for å overbevise kompilatoren om å avgrense first.postalCode fra string til keyof typeof postalCodes. Jeg tror ikke noen av de innebygde kontrollflyt-typebeskyttelsene vil gjøre denne typen innsnevring for deg (first.postalCode in postalCodes fungerer som en typebeskyttelse i noen tilfeller, men bare for å avgrense typen til postalCodes, noe som ikke er det du ønsker.) Heldigvis kan du implementere en brukerdefinert typebeskyttelse for å få den oppførselen du ser etter:

function isKeyof<T extends object>(obj: T, possibleKey: keyof any): possibleKey is keyof T {
  return possibleKey in obj;
}

Du kan deretter bruke det som følger:

declare const first: {postalCode: string};
if (isKeyof(postalCodes, first.postalCode)) {
  const x = postalCodes[first.postalCode];  // no error  
} else {
  // uh oh, first.postalCode is not valid
}

Merk at du må håndtere tilfellet der first.postalCode ikke er en av nøklene i postalCodes, noe du virkelig bør gjøre, hvis alt du vet om first.postalCode er at det er en string.

Advarsel: isKeyOf(obj, key) er generelt ikke fullstendig typesikker. Det er mulig i TypeScript for en verdi obj å ha flere egenskaper enn kompilatoren vet om i keyof typeof obj. Det vil si at typer ikke er eksakte. I det mest ekstreme eksemplet, hvis obj er deklarert som typen {}, så er keyof typeof obj never, til tross for at obj godt kan ha egenskaper. Dette er grunnen til at den vanlige forespørselen om å få Object.keys(obj) til å returnere Array<keyof typeof obj> alltid blir avvist.

Heldigvis for oss er denne forbeholden ikke et problem for objektliteraler med utledede typer som postalCodes. Det er fordi du vet sikkert at typeof postalCodes er eksakt; det finnes ingen ekstra egenskaper å bekymre seg for. Generelt er det ikke trygt å innsnevre key til keyof typeof obj, som vist av

Håper det hjelper; lykke til!

Relaterte artikler