Using TypeScript generics to efficiently differentiate nested objects within a parsed string

Consider the following type code:

const shapes = {
    circle: {
        radius: 10
    },
    square: {
        area: 50
    }
}

type ShapeType = typeof shapes
type ShapeName = keyof ShapeType

type ParsedShape<NAME extends ShapeName, PROPS extends ShapeType[NAME]> = {
    name: NAME,
    properties: PROPS
}

Now, the goal is to utilize the key of the shapes object as the shape name during serialization. However, upon deserialization, it should be possible to determine which shape was being referenced. The deserialization code looks like this:

const parseShape = (json: string): ParsedShape<ShapeName, ShapeType[ShapeName]> => {
    const parsed = JSON.parse(json)

    return {
        name: parsed.name,
        properties: parsed.properties
    }
}

The issue arises when attempting to discriminate between properties of the shape using the name:

const parsed = parseShape('{"name": "square", "properties": {"area": 50}}')

if (parsed.name === 'square') {
    //ERROR
    //Property area does not exist on type { radius: number; } | { area: number; } 
    //Property area does not exist on type { radius: number; }
    console.log(parsed.properties.area)
}

It seems that TypeScript fails to recognize that the check is for the shape name and thus doesn't narrow down the properties accordingly.

Is there a viable solution to achieve this requirement, or is it unattainable?

An interim solution currently implemented involves the following workaround, which I would prefer to avoid if feasible:

type ParsedShape<NAME extends ShapeName> = {
    [shapeName in NAME]?: ShapeType[shapeName]
}

const parseShape = (json: string): ParsedShape<ShapeName> => {
    const parsed = JSON.parse(json)

    return {
        [parsed.name]: parsed.properties
    }
}

const parsed = parseShape('{"name": "square", "properties": {"area": 50}}')

if (parsed.square) {
    console.log(parsed.square.area)
}

Answer №1

One potential solution could be to implement a universal type guard.

The UniversalShapeGuard type requires a single type parameter that represents the expected shape name. The createShapeGuard function then creates a type guard function that can narrow down the type based on the specified shape name.

type UniversalShapeGuard<T extends ShapeName> = (obj: ParsedShape<ShapeName, any>) => obj is ParsedShape<T, ShapeType[T]>;

const shapeGuard = <T extends ShapeName>(name: T): UniversalShapeGuard<T> =>
    (obj: ParsedShape<ShapeName, any>): obj is ParsedShape<T, ShapeType[T]> => obj.name === name;

const parsed = parseShape('{"name": "square", "properties": {"area": 50}}');

if (shapeGuard('circle')(parsed)) {
    console.log(parsed.properties.radius)
}

Check out the test here

Answer №2

To accomplish your goal, follow these steps:

type Keys<T> = keyof T;
type DiscriminatedUnionOfRecord<
    A,
    B = {
        [Key in keyof A as "_"]: {
            [K in Key]: [
                { [S in K]: A[K] extends A[Exclude<K, Keys<A>>] ? never : A[K] }
            ];
        };
    }["_"]
> = Keys<A> extends Keys<B>
    ? B[Keys<A>] extends Array<any>
    ? B[Keys<A>][number]
    : never
    : never;

const shapes = {
    circle: {
        radius: 10
    },
    square: {
        area: 50
    }
};

type ShapeType = DiscriminatedUnionOfRecord<typeof shapes>;
type ShapeName = keyof typeof shapes;

type PraseShape<X extends ShapeName> = { [Obj in ShapeType as "_"]: { [Prop in keyof Obj as "_"]: X extends Prop ? { name: Prop, properties: Obj[Prop] } : never } }["_"]["_"];

const parseShape = (json: string): PraseShape<ShapeName> => {
    const parsed = JSON.parse(json)

    return {
        name: parsed.name,
        properties: parsed.properties
    }
}

const parsed = parseShape('{"name": "square", "properties": {"area": 50}}');

if (parsed.name === 'square') {

    console.log(parsed.properties.area)
    //                    ^? (property) properties: { area: number; }
}

if (parsed.name === "circle") {
    console.log(parsed.properties.radius);
    //                  ^? (property) properties: { radius: number; }
}

For additional reference and to test the code, here is a Playground link. Feel free to point out any missed requirements.

Answer №3

After receiving multiple answers, I was able to steer myself in the right direction and eventually found the solution I was looking for. By incorporating the code recommended by @0xts and seeking input from GPT4 on how to simplify it further, I made significant progress towards achieving my desired outcome.

I realized that the workaround I had been utilizing was already quite close to reaching my goal, especially with the }[NAME] snippet at the end of the type declaration:

type ParsedShape<NAME extends ShapeName> = {
    [shapeName in NAME]: {
        name: shapeName
        properties: ShapeType[shapeName]
    }
}[NAME]

const parseShape = (json: string) => {
    return JSON.parse(json) as ParsedShape<ShapeName>
}

const parsed = parseShape('{"name": "square", "properties": {"area": 50}}')

if (parsed.name === 'square') {
    console.log(parsed.properties.area)
}

A big thank you to everyone who contributed!

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

Understanding the basics of reading a JSON object in TypeScript

Displayed below is a basic JSON structure: { "carousel": [], "column-headers": [{ "header": "Heading", "text": "Donec sed odio dui. Etiam porta sem malesuada magna mollis euismod. Nullam id dolor id nibh ultricies vehicula ut id el ...

The argument representing 'typeof Store' cannot be assigned to the parameter representing 'Store<AppState>'

I'm encountering an issue while trying to expand a service in Angular that utilizes ngrx. The error message I'm receiving is as follows: Argument of type 'typeof Store' is not assignable to parameter of type 'Store<AppState>& ...

Typescript: Removing signatures with a filter

I am encountering a TypeScript error stating that .filter has no signatures. I'm unsure of how to resolve this issue. interface IDevice { deviceId: string; deviceName?: string; } const joinRoom = ({ userId, deviceId, deviceName }: IRoomParams ...

Utilizing TypeScript to Retrieve the Parameter Types of a Method within a Composition Class

Greetings to the TS community! Let's delve into a fascinating problem. Envision having a composition interface structured like this: type IWorker = { serviceTask: IServiceTask, serviceSomethingElse: IServiceColorPicker } type IServiceTask = { ...

Utilizing a method from a separate class in Ionic 2

Having trouble using the takePicture() function from camera.ts in my home.ts. I keep getting an error message saying "No provider for CameraPage!" Any assistance on how to resolve this issue would be greatly appreciated, as I am new to this language and ju ...

When viewing a React data table in Chromium browsers, the columns on the right side may flicker when the screen is small or the browser

I recently integrated the React data grid Npm package by adazzle. You can find more information about it here. I encountered an issue which you can see in this example: https://codesandbox.io/s/react-data-grid-example-9sb93?file=/src/App.tsx When using a ...

Dealing with a missing item in local storage in an Angular application

When working with local storage, I have a function that saves certain values and another method that reloads these values. However, what is the best approach to handle missing items in the local storage? This could happen if a user deletes an item or if it ...

Issue with Socket.IO: socket.on not executed

Recently, I devised a custom asynchronous emitter for implementing a server -> client -> server method. Regrettably, the functionality is not meeting my expectations. Although it emits the event, it fails to execute the callback as intended. Upon a ...

Is there a method for verifying the application signature in Ionic?

For the past 2 days, I've been on a quest to find information about app certificate validation libraries/functions in Ionic. After discovering SignatureCheck.java for Android (link: enter link description here), I wonder if there is a similar solution ...

Issue with ng2-charts not rendering properly on the client side when utilized in Angular version 2.0.0-beta-17

Struggling with using ng2-charts in my Angular 2 app and encountering some challenges. app.ts import {Component} from 'angular2/core'; import {CHART_DIRECTIVES} from 'ng2-charts/ng2-charts'; @Component({ selector: & ...

Universal - Permissible data types for function and constructor arguments

In many statically typed languages, it is common to specify a single type for a function or constructor parameter. For example: function greet(name: string) { ... } greet("Alice") // works greet(42) // error TypeScript is an extension of JavaScri ...

Transfer the data stored in the ts variable to a JavaScript file

Is it possible to utilize the content of a ts variable in a js file? I find myself at a loss realizing I am unsure of how to achieve this. Please provide any simple implementation suggestions if available. In my ts file, there is a printedOption that I w ...

Transferring information between Puppeteer and a Vue JS Component

When my app's data flow starts with a backend API request that triggers a Vue component using puppeteer, is there a way to transfer that data from Backend (express) to the vue component without requiring the Vue component to make an additional backend ...

The NGRX + Resolver issue arises when the component loads before the action is fully dispatched, causing interaction problems

I'm currently facing an issue with fetching data from a route and loading it into my state before displaying my detail component. To tackle this, I've implemented a resolver. Although my get request seems to be functioning, it appears that the AP ...

Encountering an issue with managing promises in Observables for Angular HTTP Interceptor

Currently, I am encountering the following situation: I have developed an authentication service using Angular/Fire with Firebase authentication. The authentication service is expected to return the ID token through the idToken observable from Angular/Fir ...

Using lambda expressions to sort through an array of objects in React

My goal is to create a delete button that removes items from a list and updates the state variable accordingly. public OnDeleteClick = (): void => { const selectionCount = this._selection.getSelectedCount(); let newArray = this.state.items; for ...

Encountering issues with bidirectional data binding functionality

I have integrated a pagination component from ng-bootstrap into a generic component that includes a select dropdown to choose the number of items per page. I triggered an event from this generic component and caught it in the parent component (member-list. ...

The error message indicates that the argument cannot be assigned to the parameter type 'AxiosRequestConfig'

I am working on a React app using Typescript, where I fetch a list of items from MongoDB. I want to implement the functionality to delete items from this list. The list is displayed in the app and each item has a corresponding delete button. However, when ...

Issue occurred while trying to render a React component with Typescript and WebPack

I am in the process of creating a basic React component that simply displays a page saying Hello. However, I'm encountering an error in my console. My compiler of choice is TypeScript. To set up my project, I am following this guide: https://github.co ...

How to retrieve static attributes while declaring an interface

class A { public static readonly TYPE = "A"; } interface forA { for: A.TYPE } I am facing an issue while trying to access A.TYPE from the forA interface in order to perform type guarding. The error I encounter is: TS2702: 'A' only refe ...