Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Typescript, how to import json and dynamically lookup by key?

Tags:

typescript

In typescript 3.0.3, I import a json file like this:

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

It has this format:

{
    "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

I want to use it like this:

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

I get the error: "Element implicitly has an 'any' type because type '...very long type signature...' has no index signature."

Is there a way to get this working with the automatically generated json types, so that I can look up a postal code by it's string key dynamically?

My best approach right now is to have an intermediary ts file like:

import postalCodes from './PostalCodes.json';

export const PostalCodesLookup = postalCodes as {
    [key: string]: { code: number, city: string, isPoBox: boolean }
};
like image 793
asgerhallas Avatar asked Nov 28 '18 12:11

asgerhallas


2 Answers

Since TypeScript v2.9 you can enable resolveJsonModuleflag in compilerOprions of your tsconfig.json file like so:

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

Now TypeScript should automatically resolve the types in your imported json file.

To work around the index type issue, I could suggest two options:

  1. Enable suppressImplicitAnyIndexErrors in your tsconfig.json. This will suppress this error message. You won't get any type hints either.

  2. Create some types for the JSON, and use those instead of just 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"'.
    ];
    

    Depending on how "hard-coded" the usage will be, exporting the PostalCode type could work out well for you.

    You could also write a function that checks against the JSON at run-time:

    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
    }
    

Typed Autocompletion example

like image 121
hlfrmn Avatar answered Sep 27 '22 20:09

hlfrmn


I think your main issue here is not that the compiler needs to infer the type of postalCodes in some different way, but that first.postalCode is not known to be one of the keys of postalCodes. Since first.postalCode is of type string, it is reasonable for the compiler to warn you about this.

So you need to do some kind of type guard to convince the compiler to narrow first.postalCode from string to keyof typeof postalCodes. I don't think any of the builtin control-flow type guards will do this sort of narrowing for you (first.postalCode in postalCodes does act as a type guard in some cases but only to narrow the type of postalCodes, which is not what you want.) Luckily you can implement a user-defined type guard to give you the behavior you're looking for:

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

You can then use it as follows:

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
}

Note that you do have to handle the case where first.postalCode is not one of the keys of postalCodes, which is something you really should do, if all you know about first.postalCode is that it's a string.

Caveat: isKeyOf(obj, key) is not fully type safe in general. It's possible in TypeScript for a value obj to have more properties than the compiler knows about in keyof typeof obj. That is, types are not exact. In the most extreme example, if obj is declared as type {}, then keyof typeof obj is never, despite the fact that obj may well have properties. This is why the common request to have Object.keys(obj) return Array<keyof typeof obj> is always rejected.

Luckily for us, this caveat is not an issue for object literals with inferred types like postalCodes. That's because you know for sure that typeof postalCodes is exact; there are no extra properties to worry about. in general it's not safe to narrow key to keyof typeof obj, as shown by

Hope that helps; good luck!

like image 31
jcalz Avatar answered Sep 27 '22 19:09

jcalz