Typescript: defining an interface that inherits properties from a JSON type

When working with TypeScript, I've utilized a generic JSON type as suggested in this source:

type JSONValue = 
 | string
 | number
 | boolean
 | null
 | JSONValue[]
 | {[key: string]: JSONValue}

My goal is to cast interface types matching JSON to and from the JSON type. For instance:

interface Foo {
  name: 'FOO',
  fooProp: string
}

interface Bar {
  name: 'BAR',
  barProp: number;
}

const genericCall = (data: {[key: string]: JSONValue}): Foo | Bar | null => {
  if ('name' in data && data['name'] === 'FOO')
    return data as Foo;
  else if ('name' in data && data['name'] === 'BAR')
    return data as Bar;
  return null;
}

Unfortunately, TypeScript currently raises an error because it doesn't recognize how the interface could match JSONValue:

Conversion of type '{ [key: string]: JSONValue; }' to type 'Foo' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  Property 'name' is missing in type '{ [key: string]: JSONValue; }' but required in type 'Foo'.

However, logically we know that at runtime types Foo and Bar are indeed compatible with JSON. So, how can I convince TypeScript that this casting is valid?

ETA: While I can follow the error message and cast to unknown first, I prefer not to do that -- I'd like TypeScript to understand the distinction. Is that achievable?

Answer №1

The problem arises when the compiler fails to utilize the check

if ('name' in data && data['name'] === 'FOO')
for narrowing down the type of data from its original state of {[key: string]: JSONValue}. The type {[key: string]: JSONValue} is not a union, and at present, the in operator checks only narrow values of union types. There is an ongoing feature request at microsoft/TypeScript#21732 to enable such narrowing, but as of now, it's not included in the language.

This means that data remains of type {[key: string]: JSONValue} even after the check. When you attempt to assert that data is of type Foo through data as Foo, the compiler sends a warning indicating that there may be a mistake since it doesn't perceive Foo and {[key: string]: JSONValue} as closely related types.

If you are confident in the validity of your check, you can follow the compiler's suggestion and perform a type assertion to an intermediary type that is linked to both Foo and {[key: string]: JSONValue}, like unknown:

return data as unknown as Foo; // okay

If this concerns you, you have the option to create your own user-defined type guard function that carries out the desired narrowing similar to what would happen with

if ('name' in data && data['name'] === 'FOO')
. Essentially, if that condition is met, we can conclude that data is of type {name: 'FOO'}, which is sufficiently correlated to Foo for a type assertion to be valid. Here's an example of a potential type guard function:

function hasKeyVal<K extends PropertyKey, V extends string | number |
  boolean | null | undefined | bigint>(
    obj: any, k: K, v: V): obj is { [P in K]: V } {
  return obj && obj[k] === v;
}

Instead of using

if ('name' in data && data['name'] === 'FOO')
, you would write
if (hasKeyVal(data, 'name', 'FOO'))
. The return type obj is {[P in K]: V} indicates that upon returning true, the compiler should narrow the type of obj to something featuring a property with a key of type K and a value of type
V</code. Let's put it to the test:</p>
<pre><code>const genericCall = (data: { [key: string]: JSONValue }): Foo | Bar | null => {
  if (hasKeyVal(data, 'name', 'FOO'))
    return data as Foo; // okay, data is now {name: 'FOO'} which matches Foo
  else if (hasKeyVal(data, 'name', 'BAR'))
    return data as Bar;  // okay, data is now {name: 'BAR'} which corresponds to Bar
  return null;
}

Now it functions correctly. The hasKeyVal() verification narrows down data to include a name property of the correct type, making it adequately tied to either Foo or Bar for the type assertion to succeed (even though the type assertion is essential since a value of type {name: 'Foo'} may not necessarily be a Foo if Foo contains additional properties).

Playground link to code

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

Angular: proper dependency injection may not occur when appending .js or .ts at the end of an import statement

When it comes to import statements, the format is usually as follows: import {HelpService} from '../../help.service' It's worth noting that if I utilize autowiring to inject HelpService into the constructor, an already existing instance of ...

How do RxJS collection keys compare?

Is there a more efficient way to compare two arrays in RxJS? Let's say we have two separate arrays of objects. For example: A1: [{ name: 'Sue', age: 25 }, { name: 'Joe', age: 30 }, { name: 'Frank', age: 25 }, { name: & ...

Angular 5: How to Calculate the Sum of Two Numbers and Handle NaN Output

I have encountered an issue where I am trying to multiply two numbers and add another number, but the output is displaying as NaN. How can I troubleshoot and solve this problem? Below is the code snippet: medicines = [new Medicine()]; this.sum = 0;// su ...

Having trouble implementing object type switching in Typescript

While in the process of developing an angular application, I stumbled upon a peculiar issue. Some time ago, I crafted this piece of code which performed flawlessly: selectedGeoArea: any receiveStoreEvent(event) { switch (event.constructor) { ca ...

Automate the process of opening an ngbpopover from an Angular 2 component using programming techniques

Currently, I am referring to this specific article in order to integrate Bootstrap with Angular 2. While the instructions in the article are helpful, there seems to be a lack of information on how to pass the popover reference to a component method. The on ...

Determine the sum of exported identifiers based on ESLint rules

Currently, I am facing a requirement in my JavaScript/TypeScript monorepo to ensure that each library maintains a minimal amount of exported identifiers. Is there any existing eslint rule or package available that can keep track of the total number of exp ...

Launching URLs from JSON data in the system browser - Apache Cordova

I am facing an issue with opening links from a JSON in the system browser on both Android and iOS devices. The link generated by the function appears as: {{item.addressLink}}, instead of bit.ly/xyzxyz Here is what I currently have: <a href="#" ng-cl ...

How to exclude specific properties from JSON serialization in Spring MVC using Jackson?

Issue: I am looking for a simple way to exclude certain class properties (such as fields that should not be publicly exposed without authorization) when an object is returned in the @RestController method. class Article { String title; String c ...

Integrating json file from php into ReactJs

I'm having some trouble and could use some help. I have a JSON file that is generated in PHP, and I need to transfer this file to a React JS component using Ajax. However, something seems to be going wrong. The console is showing an error with the JSO ...

Combining and organizing JSON arrays

Having an issue with jq. Received the following data from API: { "items": [4,5,1,3,2], "objectNumbers": [ { "type": "objectNumber", "number": 8 }, { "type": "objectNumber", "number": 7 }, { ...

Can someone provide guidance on how to search through a set of JSON objects coming from a Common Table Expression (CTE) in PostgreSQL

I am working on a PostgreSQL query that utilizes a CTE in which the SELECT statement within the CTE uses json_agg() to aggregate data as JSON objects. My task is to search for a specific object within the array generated by the json_agg() based on the valu ...

How can I retrieve JSON data from within a function and show it on a textfield in Swift programming language?

I've been exploring how to parse JSON data into Swift, specifically attempting to display the area of my pincode in a text field. In order to do this, I've created three Swift files: mapview.swift, maphandler.swift, and mapmodel.swift. The issue ...

Unable to generate a JSON string from the JavaScript object

I need help converting the object I receive from the server into a JSON file for use in a d3.js chart: data = { "dog ":"5", "cat ":"4", "fish ":"12", } The desired output is: { "name" : "animal", "children" : [ {"name":"dog" ...

I am interested in creating an input range slider using React and TypeScript

This code was used to create a slider functionality import { mainModule } from 'process'; import React, { useState } from 'react'; import styled from 'styled-components'; const DragScaleBar = () => { const [value, setV ...

Customizing Components in Angular 2/4 by Overriding Them from a Different Module

Currently, I am utilizing Angular 4.3 and TypeScript 2.2 for my project. My goal is to develop multiple websites by reusing the same codebase. Although most of the websites will have similar structures, some may require different logic or templates. My p ...

Debug errors occur when binding to computed getters in Angular 2

Currently, I am integrating Angular 2 with lodash in my project. Within my model, I have Relations and a specific getter implemented as follows: get relationsPerType() { return _(this.Relations) .groupBy(p => p.Type) .toPairs() ...

Tips for changing date format within Ajax javascript code

I am working with a JavaScript AJAX code: success:function(res){ var _html=''; var json_data=$.parseJSON(res.posts); $.each(json_data,function (index,data) { _html+='<span class=&apo ...

Rails 5 shows an error message: "NoMethod Error - Undefined Method Paginate"

My Rails 5 JSON API has a method that retrieves a list of products for a specific company. I want to implement pagination to prevent loading all the data at once. In my search, I found two gems - will_paginate and api-pagination. After installing the gems ...

Tips for transforming an html form into an ajax request for a rest controller when dealing with a multi-select field

I am encountering an issue with the field "roles" as it is a select list with multiple choices. I need to convert this field into an array, but I have not been able to find any information on how to do so. Any assistance would be greatly appreciated. Thi ...

Learn the process of sending a delete request to a REST API with Angular

Is there a way to effectively send a delete request to a REST API using Angular? I am attempting to send a delete request with an ID of 1 My current approach is as follows: this.http.delete(environment.apiUrl+"id="+1).subscribe(data => { }); The va ...