Using keyof on an indexed property within a generic type in Typescript does not effectively limit the available options

Imagine having an interface structure like this:

export interface IHasIO {
  inputs: {
    [key: string]: string
  },
  outputs: {
    [key: string]: string
  }
}

The goal is to develop a function that adheres to this interface as a generic type and ensures that one of the output keys is provided as a parameter.

The desired outcome would be achieved with the following type definitions:

// extracting the 'outputs' property by indexing it.
export type Outputs<T extends IHasIO> = T['outputs'];

// restricting the function parameter to only allow keys present in the 'outputs'.
export type writeToOutput<T extends IHasIO> = (param: keyof Outputs<T>) => void;

However, when implementing a value that complies with the interface and using it as the generic argument, the parameter options are not properly restricted:

const instance: IHasIO = {
  inputs: {},
  outputs: {
    a: 'someValue',
    b: 'someOtherVal'
  }
}

// defining a dummy function
const fn: writeToOutput<typeof instance> = (param) => {
}

// despite 'c' not being one of the output keys, it does not trigger TypeScript linting errors
fn("c");

// only these should work without any issues:
fn("a");
fn("b");

Why is this happening? What could be going wrong?

Answer №1

The issue at hand arises from explicitly defining the type of instance as IHasIO. By doing this, you are instructing the compiler to disregard tracking its specific properties; instead, it will only recognize that instance belongs to type IHasIO, hence the inputs and outputs properties are inferred as type {[key: string]: string}, resulting in

keyof typeof instance["outputs"]
being simply string. Consequently, fn() will now accept any string as input.

If you prefer stricter typing, allow the compiler to infer the type of instance by omitting the annotation. If ensuring that instance is assignable to

IHasIO</code without converting it to that type is crucial, TypeScript 4.9 introduces <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-4-9-rc/#satisfies" rel="nofollow noreferrer">the <code>satisfies
operator:

const instance = {
  inputs: {},
  outputs: {
    a: 'someValue',
    b: 'someOtherVal'
  }
} satisfies IHasIO;

Regardless of using satisfies or not, the type of instance is automatically inferred as:

/* const instance: {
    inputs: {};
    outputs: {
        a: string;
        b: string;
    };
} */

Hence,

keyof typeof instance["outputs"]
becomes "a" | "b". Consequently, fn() functions as intended:

fn("c"); // error! 
// ~~~
// Argument of type '"c"' cannot be assigned to 
// parameter of type '"a" | "b"'.
fn("a"); // okay
fn("b"); // okay

The problem here is that by explicitly annotating the type of instance as IHasIO, you have essentially told the compiler that it should not try to keep track of its particular properties; instead it will only know that instance is of type IHasIO, and thus the inputs and outputs properties are of type {[key: string]: string}, meaning that

keyof typeof instance["outputs"]
is just string. And therefore fn() will accept any string as input.

If you would like stronger typing, you should just let the compiler infer the type of instance, by leaving off the annotation. If you really care about verifying that instance is assignable to IHasIO without widening it to that type, you can use the satisfies operator which will be released in TypeScript 4.9:

const instance = {
  inputs: {},
  outputs: {
    a: 'someValue',
    b: 'someOtherVal'
  }
} satisfies IHasIO;

But with or without satisfies, the type of instance is inferred to be

/* const instance: {
    inputs: {};
    outputs: {
        a: string;
        b: string;
    };
} */

And therefore

keyof typeof instance["outputs"]
is "a" | "b". And so fn() now behaves as desired:

fn("c"); // error! 
// ~~~
// Argument of type '"c"' is not assignable to 
// parameter of type '"a" | "b"'.
fn("a"); // okay
fn("b"); // okay

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

Using React and Typescript: Passing functions as props to other components

In this setup, we have three main components: Toggle, ToggleMenu, and Wrapper. The Toggle component is designed to be universal and used for various functions. The Wrapper component, on the other hand, is meant to change the background color only when the ...

What is the most effective approach for annotating TypeScript abstract classes that are dynamically loaded?

I am in the process of developing a library that allows for the integration of external implementations, and I am exploring the optimal approach to defining types for these implementations. Illustration abstract class Creature { public abstract makeN ...

Is it possible to integrate TypeScript 5.0 decorators into React components?

Every time I add decorators to my class, they always get called with the arguments specified for legacy decorators: a target, property key, and property descriptor. I am interested in using TypeScript 5.0 decorators. Is this feasible, and if so, how can I ...

Command to update a document in AWS DynamoDB using the Document Client

While attempting to utilize the UpdateCommand feature within the AWS DynamoDB documentation, I encountered various challenges due to its lack of detailed explanation and difficulty in implementation. My aim was to employ the update command to seamlessly t ...

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 ...

Tips on typing the onFocus function event parameter for a Material UI Input component

Currently, I am working on a custom dropdown using material ui components like Input and Popper. The goal is to have the popper open when the user focuses on the input field. Additionally, I am implementing this solution with TypeScript. import ClickAwayL ...

The Vite proxy server will not modify POST requests

When I set up a proxy using Vite, I noticed that it only handles GET and HEAD requests. I'm looking to have other request methods proxied as well. In a new Vite React project - the only modification I made was in vite.config.ts import { defineConfig ...

Sort your list efficiently with a custom hook in React using Typescript

I've been working on developing a custom hook in React that sorts an array based on two arguments: the list itself and a string representing the key to sort by. Despite trying various approaches, I haven't been able to find a solution yet. I&apos ...

Is there a shortcut for creating interfaces that have identical sub properties?

We are seeking to streamline the interface creation process by utilizing shorthand for properties labeled from Monday through Sunday, each with identical sub-properties. interface Day { start: number end: number } interface Schedule { Monday: Day ...

What could be causing the error that pops up every time I attempt to execute a git push

When I executed the following command in git git push origin <the-name-of-my-branch> I encountered the following warning message Warning: The no-use-before-declare rule is deprecated since TypeScript 2.9. Please utilize the built-in compiler check ...

Does the term 'alias' hold a special significance in programming?

Utilizing Angular 2 and Typescript, I have a component with a property defined as follows: alias: string; Attempting to bind this property to an input tag in my template like so: <input class="form-control" type="text" required ...

Is it possible to schedule deployments using Vercel Deploy Hooks in Next.js?

When we schedule a pipeline on git, I want to schedule deploy hooks on vercel as well. Since the app is sending getStaticProps and every HTTP request will be run on every build, I have to rebuild the site to get new results from the server. For instance, ...

Angular 8's array verification feature lacks the ability to recognize preexisting elements

I've been trying to add and delete items in an array when a user selects or deselects the same item. However, it appears that either my array is not working properly or there is a bug in my code causing it to fail. <div class="grp-input"> ...

Emphasize x-axis heading in a Highcharts graph

In my Highcharts bar graph, I am looking for a way to dynamically highlight the x-axis label of a specific bar based on an external event trigger. This event is not a standard click interaction within the Highcharts graph. Currently, I have been able to r ...

Creative Solution for Implementing a Type Parameter in a Generic

Within my codebase, there exists a crucial interface named DatabaseEngine. This interface utilizes a single type parameter known as ResultType. This particular type parameter serves as the interface for the query result dictated by the specific database dr ...

Using Angular2: Implementing a single module across multiple modules

Let's delve into an example using the ng2-translate plugin. I have a main module called AppModule, along with child modules named TopPanelModule and PagesModule. The ng2-translate is configured for the AppModule. @NgModule({ imports: [TranslateMo ...

Dynamically modifying the display format of the Angular Material 2 DatePicker

I am currently utilizing Angular 2 Material's DatePicker component here, and I am interested in dynamically setting the display format such as YYYY-MM-DD or DD-MM-YYYY, among others. While there is a method to globally extend this by overriding the " ...

Retrieving Data in Typescript Async Function: Ensuring Data is Returned Once All Code is Executed

I need help with waiting for data to be retrieved before returning it. The code below fetches data from indexedDB and sends it back to a component. I understand that observables or promises can accomplish this, but I am struggling with how to implement t ...

Guide on Combine Multiple Observables/Subscriptions into a Nest

1. A Puzzle to Solve I am faced with the challenge of implementing a dynamic language change flow for my blog. Here is an overview of how I envision it: The user initiates a language change by clicking a button that triggers an event (Subject). This eve ...

How can you establish the default value for a form from an Observable?

Check out my TypeScript component below export interface Product{ id?:string, name:string, price:string; quantity:string; tags:Tags[]; description:string; files: File[]; } product$:Observable<Product | undefined>; ngOnIn ...