What is the process in Typescript for importing JSON files and dynamically searching for values based on keys?

When working with typescript 3.0.3, I encountered an issue while importing a json file in the following manner:

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

The json file has the structure shown below:

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

The goal is to utilize it as demonstrated below:

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

However, this results in the error: "Element implicitly has an 'any' type because type '...very long type signature...' has no index signature."

Is there a method to make this function properly using the automatically generated json types, allowing for dynamic lookup of a postal code based on its string key?

Currently, my workaround involves creating an intermediary ts file like:

import postalCodes from './PostalCodes.json';

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

Answer №1

Starting from TypeScript version 2.9, you have the option to turn on the resolveJsonModule flag within the compilerOptions section of your tsconfig.json file like shown below:

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

With this set up, TypeScript will automatically resolve types in your imported json files.

To tackle the index type issue, here are two suggestions:

  1. Enable suppressImplicitAnyIndexErrors in your tsconfig.json. This will eliminate the error message but also remove any type hints.

  2. Create specific types for the JSON data and use them instead of just using string:

    import codes from '../codes.json';
    type PostalCode = keyof typeof codes;
    
    const goodString: 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,
        goodString, // 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"'.
        goodString 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 fixed the usage will be, exporting the PostalCode type could be beneficial.

    You can also create a function that dynamically validates against the JSON at runtime:

    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; // raises an issue regarding the index signature
    }
    

https://i.stack.imgur.com/elb9q.png

Answer №2

It seems that the main issue you're facing is not related to how the compiler infers the type of postalCodes, but rather the fact that first.postalCode may not be a valid key within postalCodes. Since first.postalCode is defined as a type string, it makes sense for the compiler to raise a warning in this scenario.

In order to address this, you'll need to utilize a form of type guard to explicitly narrow down the type of first.postalCode from string to keyof typeof postalCodes. The built-in control-flow type guards won't provide this level of narrowing (first.postalCode in postalCodes does serve as a type guard in certain cases, but only for narrowing the type of postalCodes, which isn't your objective). Thankfully, you can create a custom type guard to achieve the desired behavior:

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

You can then apply it as illustrated below:

declare const first: {postalCode: string};
if (isKeyof(postalCodes, first.postalCode)) {
  const x = postalCodes[first.postalCode];  // no error  
} else {
  // Oops, looks like first.postalCode is invalid
}

Do keep in mind that you must handle scenarios where first.postalCode isn't among the keys of

postalCodes</code — an essential practice when all you know about <code>first.postalCode
is it's a string.

Note: While isKeyOf(obj, key) is generally not entirely type safe, it's feasible in TypeScript for an object obj to possess additional properties unknown to the compiler in keyof typeof obj. Put differently, TypeScript types aren't inherently exact. This inconsistency explains why requests suggesting Object.keys(obj) should yield Array<keyof typeof obj> are consistently turned down.

Fortunately, this discrepancy doesn't affect object literals with inferred types like postalCodes. Such instances guarantee precise knowledge of typeof postalCodes; there are no extraneous properties to fret over. On the whole, narrowing key to keyof typeof obj poses risks, as explained above.<

I hope this clarification proves helpful! Best of luck!

Answer №3

If you're looking to incorporate JSON files into your JavaScript code, consider utilizing dynamic imports as well.

Here's an example:

const countries: { [key: string]: { name: string, population: number } } = await import('../Countries.json');
const selectedCountry = countries['US']; // now works seamlessly

You can also use explicit interface or type declarations for better organization:

interface Countries {
  [key: string]: { name: string, population: number }
}

const countries: Countries = await import('../Countries.json');
const selectedCountry = countries['CA']; // should work smoothly as well

To fully enable dynamic imports in TypeScript, make sure to set the resolveJsonModule compiler option to true and choose a suitable value for the module compiler option such as es2020, commonjs, etc.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What's the quickest method for duplicating an array?

What is the quickest method for duplicating an array? I wanted to create a game, but I found that Array.filter was performing too slowly, so I developed a new function: Array.prototype.removeIf = function(condition: Function): any[] { var copy: any[] ...

Numerous mistakes detected in the TypeScript code

I've implemented the following class within an ASP.NET Core React application: import * as React from 'react'; interface MyInputProps { inputType: string; id: string; className: string; parentFunctio ...

Error encountered in Angular with Karma and Jasmine: The function this.Service.<foo> is not defined in the Lifecycle Hook

When running Karma and Jasmine tests using the npm run test -- --no-watch --no-progress command with Karma/Jasmine, an error is thrown: Chrome 92.0.4515.159 (Mac OS 10.15.7) LoginComponent should create FAILED TypeError: this.loggerService.onDebug is n ...

Encountering a Typescript error with Next-Auth providers

I've been struggling to integrate Next-Auth with Typescript and an OAuth provider like Auth0. Despite following the documentation, I encountered a problem that persists even after watching numerous tutorials and mimicking their steps verbatim. Below i ...

"TypeORM's createConnection function on MacOS with PG database returns a Pending status even when using

Running MacOS Catalina version 10.15.4 To replicate the issue, follow these steps using the quick guide: npm install typeorm --save npm install reflect-metadata --save npm install @types/node --save npm install pg --save npm install typeorm -g typeorm in ...

Retrieving the unprocessed data from a Stripe webhook in Nest.js

I'm currently working on integrating a Stripe webhook request into my Nest.js application, and I need to access the raw body of the request. After referencing this example, I incorporated the following code snippet into the module containing a contro ...

Issue: The keyword in React/React-Native is returning a boolean value instead of the expected element object

I've recently delved into learning and coding with React, and I'm encountering a bug that I need help fixing. The issue lies within my application screen where I have two checkboxes that should function like radio buttons. This means that when on ...

Tips for using useState to update only the element that was clicked:

When I click the button to add a step to a chapter, it's adding a step to all chapters instead of just one. What mistake am I making? const example_chapters = [ { id: 1, title: 'Chapter 1'}, { id: 2, title: 'Chapter 2'}, ...

Proceed the flow of event propagation using the react-aria button element

In the react-aria library, event bubbling for buttons is disabled. I am facing an issue where my button, which is nested inside a div that acts as a file uploader, does not trigger the file explorer when clicked due to event bubbling being disabled. How ...

In Typescript, interfaces are required to have properties written in Pascal Case instead of camel Case

Currently, I am facing a strange issue in my ASP.NET 5 application where I am using Angular 1.4 with TypeScript and RXJS. Somehow, during the runtime, all my interface properties are getting converted from camel casing to Pascal casing. This unexpected beh ...

Tips for managing the dimensions of the <label> element within a React component

I'm facing an issue where the element size is not matching the box size as expected. Additionally, the width property doesn't seem to work in React. Does anyone know how to solve this problem? const DragDrop = () => { ... return ( &l ...

Assign a predetermined value to a dropdown list within a FormGroup

I have received 2 sets of data from my API: { "content": [{ "id": 1, "roleName": "admin", }, { "id": 2, "roleName": "user", }, { "id": 3, "roleName": "other", } ], "last": true, "totalEleme ...

Navigating through pages: How can I retrieve the current page value for implementing next and previous functions in Angular 7?

Greetings, I am a new learner of Angular and currently working on custom pagination. However, I am facing difficulty in finding the current page for implementing the next and previous functions. Can anyone guide me on how to obtain the current page value? ...

How to extract component prop types in Vue 3 with typescript for reusability in other parts of your application

When you specify the props under the "props:" key of a Vue component, Vue can already automatically determine their types, which is quite convenient. However, I am wondering if there is an utility type in Vue that can be used to extract the props' ty ...

The "main" entry for ts-node is not valid when running ts-node-dev

Recently, I embarked on a TypeScript project using yarn where I executed the following commands: yarn init -y yarn add typescript -D yarn tsc --init yarn add ts-node-dev -D Subsequently, I crafted a script titled dev that triggers tsnd src/index.ts, howev ...

Only map property type when the type is a union type

My goal is to create a mapping between the keys of an object type and another object, following these rules: Each key should be from the original type If the value's type is a union type, it should have { enum: Array } If the value's type is not ...

typescript throwing an unexpected import/export token error

I'm currently exploring TypeScript for the first time and I find myself puzzled by the import/export mechanisms that differ from what I'm used to with ES6. Here is an interface I'm attempting to export in a file named transformedRowInterfac ...

Troubleshooting Typescript compilation using tsc with a locally linked node package

In our project using React/React Native with Typescript, we have a mobile app built with React Native and a shared private package that is utilized by both the React Native client and the React frontend. Due to frequent changes in the shared package, we a ...

The Axios GET method retrieves a response in the form of a string that represents an object

I have developed a function that triggers an axios Request. I am working with typescript and I am avoiding the use of any for defining return data types of both the function and the axios request itself. The issue arises when the returned object contains ...

Ways to conceal an element in Angular based on the truth of one of two conditions

Is there a way to hide an element in Angular if a specific condition is true? I attempted using *ngIf="productID == category.Lane || productID == category.Val", but it did not work as expected. <label>ProductID</label> <ng-select ...