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

Narrowing Down State Types

I am working on a functional component in NextJS with props passed from SSR. The component has a state inside it structured like this: const MyComponent = ({err, n}: {err?: ErrorType, n?: N})=>{ const [x, setX] = useState(n || null) ... if(e ...

Activate the Keypress event to update the input value in React upon pressing the Enter

I am facing an issue where I need to reset the value of an input using a method triggered by onPressEnter. Here is the input code: <Input type="text" placeholder="new account" onPressEnter={(event) => this.onCreateAccount(event)}> < ...

How to retrieve an array stored within a JSON object

I am trying to access a specific array within an object from a JSON file. Here is the snippet of the data I'm dealing with: best-sellers": [ { "title": "Chuteira Nike HyperVenomX Proximo II Society", "price": 499.90, "installmen ...

Encountering a 405 error when making an OpenAI API call with next.js, typescript, and tailwind CSS

I encountered a 405 error indicating that the method request is not allowed. I am attempting to trigger an API route call upon clicking a button, which then connects to the OpenAI API. Unsure of my mistake here, any guidance would be highly appreciated. E ...

What sets apart 'export type' from 'export declare type' in TypeScript?

When using TypeScript, I had the impression that 'declare' indicates to the compiler that the item is defined elsewhere. How do these two seemingly similar "types" actually differ? Could it be that if the item is not found elsewhere, it defaults ...

How can you merge arrays in Angular based on their unique names?

Is there a way to combine objects within the same array in Angular based on their names? [{ "name":"Navin", "id":"1" }, { "name":"Navin", "mark1":"98" ...

Updating the variable in Angular 6 does not cause the view to refresh

I am facing an issue with my array variable that contains objects. Here is an example of how it looks: [{name: 'Name 1', price: '10$'}, {name: 'Name 2', price: '20$'}, ...] In my view, I have a list of products bei ...

What is the best way to retrieve a cookie sent from the server on a subdomain's domain within the client request headers using getServerSideProps

Currently, I have an express application using express-session running on my server hosted at api.example.com, along with a NextJS application hosted at example.com. While everything functions smoothly locally with the server setting a cookie that can be r ...

Tips for accessing and manipulating an array that is defined within a Pinia store

I have set up a store to utilize the User resource, which includes an array of roles. My goal is to search for a specific role within this array. I've attempted to use Array functions, but they are not compatible with PropType<T[]>. import route ...

Encountered an issue while using OpenAPI 3.1 with openapi-generator-cli typescript-fetch. Error: JsonParseException - The token 'openapi' was not recognized, expected JSON String

I am interested in creating a TypeScript-fetch client using openapi-generator-cli. The specifications were produced by Stoplight following the OpenAPI 3.1 format. However, when I execute the command openapi-generator-cli generate -i resources/openapi/Attri ...

Discovering the type in Typescript by specifying a function parameter to an Interface

Consider this sample interface: interface MyInterface { x: AnotherThing; y: AnotherThingElse; } Suppose we make the following call: const obj: MyInterface = { x: {...}, y: {...}, } const fetchValue = (property: keyof MyInterface) => { ...

Setting up the vscode launch configuration to enable debugging on the cloud-run emulator with TypeScript

I am currently facing an issue with debugging a Google Cloud Run application on the Cloud Run emulator. The application is built using TypeScript. While I can successfully run and debug the application locally, breakpoints are being ignored or grayed out w ...

Removing API request in React.js

My approach: deleteSample = () => { this.sampleService .deleteCall(this.props.id) .then((response) => { window.location.reload(false); }) .catch((error) => { console.log ...

The statement 'typeof import("...")' fails to meet the requirement of 'IEntry' constraint

When trying to run npm run build for my NextJS 13 app, I encountered the following type error: Type error: Type 'typeof import("E:/myapp/app/login/page")' does not satisfy the constraint 'IEntry'. Types of property 'def ...

Dealing with the error "Type 'date[]' is not assignable to type '[date?, date?]' in a React hook

I'm attempting to assign a date range but encountering an error that states: Type 'Date[]' is not assignable to type '[Date?, Date?]'. Types of property 'length' are incompatible. Type 'number' is not assignab ...

Launching the Skeleton feature in NextJS with React integration

I have been working on fetching a set of video links from an Amazon S3 bucket and displaying them in a video player component called HoverVideoPlayer. However, during the loading process, multiple images/videos scale up inside a Tailwind grid component, ca ...

What are some ways to implement Material UI's Chip array to function similar to an Angular Chip Input?

Can the sleek design of Angular Material's Chip input be replicated using a React Material UI Chip array? I am attempting to achieve the modern aesthetic of Angular Material Chip input within React. While the Material UI Chip array appears to be the ...

Is it possible to create cloud functions for Firebase using both JavaScript and TypeScript?

For my Firebase project, I have successfully deployed around 4 or 5 functions using JavaScript. However, I now wish to incorporate async-await into 2 of these functions. As such, I am considering converting these specific functions to TypeScript. My conc ...

HTMLElement addition assignment failing due to whitespace issues

My current challenge involves adding letters to a HTMLElement one by one, but I'm noticing that whitespace disappears in the process. Here's an example: let s = "f o o b a r"; let e = document.createElement('span'); for (let i ...

Compilation errors plague TSC on varying systems

After successfully creating a node app in TypeScript and running it locally without any issues, I encountered compilation errors when deploying the app on Heroku: app/api/controllers/ingredient.controller.ts(3,24): error TS2307: Cannot find module & ...