Is it possible for Typescript to deduce keys of a generic type within conditional statements?

Within my function, I have a parameter that accepts an enum value as T and a generic type Data<T> which selects between two different data types.

I expected to be able to access properties of type BarData within a conditional statement that should make T known. However, the compiler still interprets data as a union type.

The code functions correctly but how can I eliminate the TypeScript errors?

enum DataType { Foo, Bar }
interface FooData { someKey: string }
interface BarData extends FooData { otherKey: string }

type Data<T extends DataType> = T extends DataType.Foo ? FooData : BarData

function func<T extends DataType>(type: T, data: Data<T>): void {
    const getter = <K extends keyof Data<T>>(key: K): Data<T>[K] => data[key]

    if (type === DataType.Bar) {
        data; // still inferred as Data<T>
        console.log(data.otherKey) // error Property 'otherKey' does not exist on type 'FooData | BarData'.
        console.log(getter('otherKey')) // error Argument of type 'string' is not assignable to parameter of type 'keyof Data<T>'.
    }
}

Playground link

Answer №1

To ensure that invalid states are not represented, consider using rest parameters instead of generics.

enum DataType { Foo = 'Foo', Bar = 'Bar' }

interface FooData { someKey: string }

interface BarData extends FooData { otherKey: string }


type MapStructure = {
  [DataType.Foo]: FooData,
  [DataType.Bar]: BarData
}

type Values<T> = T[keyof T]

type Tuple = {
  [Prop in keyof MapStructure]: [type: Prop, data: MapStructure[Prop]]
}

// ---- > BE AWARE THAT IT WORKS ONLY IN T.S. 4.6 < -----

function func(...params: Values<Tuple>): void {
  const [type, data] = params
  const getter = <Data, Key extends keyof Data>(val: Data, key: Key) => val[key]

  if (type === DataType.Bar) {
    const foo = type
    data; // BarData
    console.log(data.otherKey) // ok
    console.log(getter(data, 'otherKey')) // ok
    console.log(getter(data, 'someKey')) // ok

  }
}

Playground

MapStructure - is used just for mapping keys with valid state.

Values<Tuple> - creates a union of allowed tuples.Since rest parameters is nothing more than a tuple, it works like a charm.

Regarding getter. You should either define it inside if condition or make it separate function. SO, feel free to move getter out of the scope of func.

If you want to stick with generics, like in your original example, you should make type and data a part of one datastracture and then use typeguard

enum DataType { Foo, Bar }
interface FooData { someKey: string }
interface BarData extends FooData { otherKey: string }

type Data<T extends DataType> = T extends DataType.Foo ? FooData : BarData

const isBar = (obj: { type: DataType, data: Data<DataType> }): obj is { type: DataType.Bar, data: BarData } => {
    const { type, data } = obj;
    return type === DataType.Bar && 'other' in data
}

function func<T extends DataType>(obj: { type: T, data: Data<T> }): void {
    const getter = <K extends keyof Data<T>>(key: K): Data<T>[K] => obj.data[key]

    if (isBar(obj)) {
        obj.data // Data<T> & BarData
        console.log(obj.data.otherKey) // ok       
    }
}

But issue with getter still exists since it depend on uninfered obj.data. You either need to move out getter of func scope and provide extra argument for data or move getter inside conditional statement (not recommended).

However, you can switch to TypeScript nightly in TS playground and use object type for argument:

enum DataType { Foo, Bar }
interface FooData { someKey: string }
interface BarData extends FooData { otherKey: string }

type Data = { type: DataType.Foo, data: FooData } | { type: DataType.Bar, data: BarData }


function func(obj: Data): void {
    const { type, data } = obj;
    const getter = <K extends keyof typeof data>(key: K): typeof data[K] => data[key]

    if (type === DataType.Bar) {
        data // BarData
        console.log(obj.data.otherKey) // ok       
    }
}

Playground

getter still does not work in a way you expect, hence I recomment to move it out from func

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

Experiencing difficulty in triggering a NextUI Modal by clicking on a NextUI Table Row

In the process of developing my web portfolio, I am utilizing NextJS, TypeScript, and TailwindCSS. A key feature on my site involves displaying a list of books I have read along with my ratings using a NextUI table. To visualize this functionality, you can ...

The ArgsTable component is not displayed in Storybook when using Vite, Typescript, and MDX

I'm struggling to display the table with props on a MDX documentation page. No matter what I try, the table only shows: "No inputs found for this component. Read the docs >" Despite trying various methods, I can't seem to get it to work. I h ...

Encountering the issue: "Property '...' is not found on the type 'typeof "...."'"

Currently, I am in the process of developing a node js application using typescript. To transpile the code, I am utilizing the gulp transpiler in commonjs mode. One of the files I've written is homeController.ts, which looks like this: let homeContr ...

Uploading files using Angular 2 and TypeScript

I am currently developing an Angular 2 project, where I need to upload a file and send some parameters from the client to the server (Spring Rest Server). I have attempted to use the FormData Interface for this purpose. However, when I try to append a file ...

Exploring the depths of Javascript objects using Typescript

If I have this specific dataset: data = { result: [ { id: '001', name: 'Caio B', address: { address: 'sau paulo', city: 'sao paulo', ...

Load images dynamically based on their filenames

As I continue to learn, I am working on a delivery layout project. I have come across a section where I need to load Cards with restaurant information. The names are displaying correctly, but I am facing issues with loading the images. I am using a JSON fi ...

Iterate over an array of objects and inspect their attributes

Having an array of objects stored in the in_timings variable, I am attempting to validate specific conditions on these objects in preparation for execution. Despite my efforts, it appears that the loop is running on sections I did not anticipate. in_timi ...

In Typescript, try/catch blocks do not capture return values

I am currently working on a function that performs database operations, with the implementation contained within a try/catch block. Here is an example: async function update({id, ...changes}): Promise<IUserResult> { try { //insert code here retu ...

Discovering a user's role in a WordPress site within an Angular application

I am currently in the process of integrating an Angular application into a WordPress theme. Certain Angular components need to verify if a user is logged in and has a specific role in order to display certain content. Since WordPress is built on PHP and An ...

The initial update of the view does not occur when a component property changes in Angular 2 RC6

I am currently facing an issue with a component in my project. This component calls a service to retrieve locally stored JSON data, which is then mapped to an array of objects and displayed in the component view. The problem I am encountering is that the v ...

How can you upload information while specifying the status?

Within my config.ts file, the contents are as follows: export const keyOrders: {} = { "aa": { active: true, order: 0 }, "bb": { active: true, order: 1 }, "cc": { active: true, order: 2 }, "dd": { active: true, order: 3 }, "ee": { activ ...

Retrieve information from subscriber and store it in a local variable

Angular 6 Service getProjectEpics(id: string): Observable<any> { return this.http.get<any>(this.baseUrl + 'getEpics/' + id); } Component Angular 6 projectEpics=[]; getProjectEpics(id: string) { this.epicService.getProjectEpics(this ...

Keep an eye on the output of Firebase database in Angular 2

Just starting out in angular, so please be patient :) Using Angular 2 (version 1.0.4), Angular CLI, and NodeJs 7.9. I've been trying to create a centralized service that checks if a user is logged in, retrieves their data, and sends it back for the ...

Why is it necessary to omit node_modules from webpack configuration?

Check out this webpack configuration file: module.exports = { mode: "development", entry: "./src/index.ts", output: { filename: "bundle.js" }, resolve: { extensions: [".ts"] }, module: { rules: [ { test: /\.ts/ ...

What is the syntax for declaring a variable with multiple types in Angular/Typescript?

Imagine having a variable declared with the following type: test?: boolean | { [key in TestEnum ]: boolean }; Now, let's assign this test variable within a constant where it can hold either a boolean value or a mapping to an enum. Consider the exampl ...

Facing Syntax Errors When Running Ng Serve with Ngrx

Currently, I am enrolled in an Angular course to gain proficiency in ngrx. In a couple of months, I will be responsible for teaching it, so I am refreshing my memory on the concept. Strangely, even after installing it and ensuring my code is error-free, er ...

Tips for sending icons as properties in React using TypeScript

Recently diving into typescript, I embarked on a straightforward project. Within this project lies a sidebar component that comprises multiple sidebarNavigationItem components. Each of these Sidebar items consists of an Icon and Title, showcased below. Si ...

Typescript for managing the Shopify admin API

Is there anyone who can confirm whether Shopify offers typescript definitions for their admin API? I'm specifically interested in finding types for Orders, Products, and Variants. I initially assumed that this package would have them, but it seems l ...

I love the idea of the music playing on as I navigate between pages or tabs. Such a cool feature with Next

I'm looking to implement a music player within my next.js application. How can I ensure that the currently playing track doesn't stop when switching between pages? Essentially, I want the music playback to continue seamlessly as users navigate th ...

What could be causing my object property to be undefined when used in an if statement?

My attempt to search through an array using a for loop is not yielding the expected results. let matrix = []; for(let i=0; i<this.row; i++){ for(let j=0; j<this.column; j++){ if(this.grid[i][j].name != ""){ ...