How can I conditionally make an object property optional in TypeScript depending on another type?

To better illustrate my situation, I believe presenting it in code would be most effective:

interface IPluginSpec {
  name: string;
  state?: any;
}

interface IPluginOpts<PluginSpec extends IPluginSpec> {
  name: PluginSpec['name'];
  // How can we enforce the requirement of opts.initialState ONLY when PluginSpec['state'] has a value?
  initialState: PluginSpec['state'];
}

function createPlugin<PluginSpec extends IPluginSpec>(
  opts: IPluginOpts<PluginSpec>,
) {
  console.log('create plugin', opts);
}

interface IPluginOne {
  name: 'pluginOne';
  // Ideally, we'd only have to define this if state is present or omitted entirely
  // state: undefined;
}

// Error: Property 'initialState' is missing in type...
createPlugin<IPluginOne>({
  name: 'pluginOne',
  // How do we make initialState NOT mandatory?
  // initialState: undefined,
  // How can we prevent any non-undefined initialState values?
  // initialState: 'anyvaluehere',
});

interface IPluginTwo {
  name: 'pluginTwo';
  state: number;
}

createPlugin<IPluginTwo>({
  name: 'pluginTwo',
  initialState: 0,
});

Answer №1

To achieve this, you can utilize a conditional type to test for the existence of a property and determine whether it should be included or excluded:

interface IPluginSpec {
  name: string;
  state?: any;
}

type IPluginOpts<PluginSpec extends IPluginSpec> = PluginSpec extends Record<'state', infer State> ? {
  name: PluginSpec['name'];
  initialState: State;
} : {
  name: PluginSpec['name']
}

function createPlugin<PluginSpec extends IPluginSpec>(
  opts: IPluginOpts<PluginSpec>,
) {
  console.log('create plugin', opts);
}

interface IPluginOne {
  name: 'pluginOne';
}

// Ok
createPlugin<IPluginOne>({
  name: 'pluginOne',
  // nothing to add
});

interface IPluginTwo {
  name: 'pluginTwo';
  state: number;
}

createPlugin<IPluginTwo>({
  name: 'pluginTwo',
  initialState: 0,
});

For a more modular approach, an intersection type can be used with common properties combined with optional parts in separate conditionals:

interface IPluginSpec {
    name: string;
    state?: any;
    config?: any;
}

type IPluginOpts<PluginSpec extends IPluginSpec> = {
        name: PluginSpec['name']
    }
    & (PluginSpec extends Record<'state', infer State> ? { initialState: State; } : {})
    & (PluginSpec extends Record<'config', infer Config> ? { initialConfig: Config; } : {})

While conditional types are valuable for callers, TypeScript may struggle to reason about them within the implementation due to unknown types (T). To address this, it is recommended to maintain a public signature with conditional types and a simplified internal implementation without such complexities. This will ensure that the function can be implemented without requiring type assertions, while still providing the expected behavior to the caller:

function createPlugin<PluginSpec extends IPluginSpec>(opts: IPluginOpts<PluginSpec>)
function createPlugin<PluginSpec extends IPluginSpec>(opts: {
    name: string
    initalState: PluginSpec['state'],
    initialConfig: PluginSpec['config'],
}) {
    if (opts.initalState) {
        opts
    }
}

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

React encountered an issue: each child element within a list must be assigned a unique "key" prop

I am feeling a bit puzzled as to why I keep getting the error message: child in a list should have a unique "key" prop. In my SearchFilterCategory component, I have made sure to add a key for each list item using a unique id. Can you help me figu ...

Verifying if properties are empty in an array of objects

I am trying to find out if there is a null value in an array of objects, and return true if there is. One issue I am facing is: The current condition always returns 'false' due to the mismatch between types '{ type: any; startDate: string; ...

Tips for accessing and adjusting an ngModel that is populated by an attribute assigned via ngFor

Looking for guidance on how to modify an input with ngModel attribute derived from ngFor, and update its value in the component. Here is my code snippet for reference: HTML FRONT The goal here is to adjust [(ngModel)] = "item.days" based on button click ...

On production, Heroku fails to load JavaScript files, only allowing CSS files to be loaded. However, the files load successfully when

I've been struggling to find a solution to my problem, so I'm reaching out for some help. I am in the process of deploying my node (express) app to Heroku, but I've encountered an issue where only CSS files from my public folder are being t ...

Looping through an array of nested objects using Vue

I have encountered a challenge with accessing specific data within an array that I am iterating over. The array is structured as follows, using Vue.js: companies: [ name: "company1" id: 1 type: "finance" additionalData: "{& ...

Opening an Accordion programmatically in an Angular 4 application

I am currently developing an Angular 4 application where I have implemented the same accordion feature in two different components. My goal is to allow the user to click on an accordion in the first component and then pass the selected index to the second ...

Angular and Node version discrepancies causing problems

This particular CLI version is designed to work with Angular versions ^11.0.0-next || >=11.0.0 <12.0.0, however an Angular version of 13.0.0 was detected instead. If you need assistance with updating your Angular framework, please refer to the follo ...

Ensuring the correct class type in a switch statement

It's been a while since I've used Typescript and I'm having trouble remembering how to properly type guard multiple classes within a switch statement. class A {} class B {} class C {} type OneOfThem = A | B | C; function test(foo: OneOfThe ...

Issue with Typescript flow analysis when using a dictionary of functions with type dependency on the key

I am utilizing TypeScript version 4.6 Within my project, there exists a type named FooterFormElement, which contains a discriminant property labeled as type type FooterFormElement = {type:"number",...}|{type:"button",...}; To create instances of these el ...

ReactJS and Redux: setting input value using properties

I am utilizing a controlled text field to monitor value changes and enforce case sensitivity for the input. In order to achieve this, I need to access the value property of the component's state. The challenge arises when I try to update this field ...

Implementing both function calls and route changes with Navilink in reactjs

When a user clicks on a navigation link in the popup, I want to not only close the popup but also redirect to a new page. The click function is working correctly, but the "to" function for routing to the new page is not functioning as expected. What is the ...

Typesafe React with TypeScript: Utilizing Types for array.map() with arrow functions

Having a background in C# but being new to js/ts, I am currently delving into React and wanting to incorporate typescript into my workflow. In an attempt to do so, I am trying to add types to a demo project located here: https://github.com/bradtraversy/pro ...

Quote the first field when parsing a CSV

Attempting to utilize Papaparse with a large CSV file that is tab delimited The code snippet appears as follows: const fs = require('fs'); const papa = require('papaparse'); const csvFile = fs.createReadStream('mylargefile.csv&apo ...

Attempting to assign incompatible types in TypeScript

I'm encountering an error that I can't quite figure out: Type '"New Choice"' is not assignable to type '"Yes" | "No"'.ts(2322) test.ts(17, 14): The expected type comes from property 'text&apo ...

What is the best way to address elements that are affected by a wrong *ngIf condition at the beginning?

I'm trying to modify the attributes and styles of DOM elements that only appear once an *ngIf condition becomes true. I'm using @ViewChild() decorator to access these elements, but I keep running into an error: Cannot read property nativeEleme ...

The process of adding new files to an event's index

I'm trying to attach a file to an event like this: event.target.files[0]=newFile; The error I'm getting is "Failed to set an indexed property on 'FileList': Index property setter is not supported." Is there an alternative solution fo ...

Clicking on a single checkbox causes the entire input to become deactivated due to the way the system is

I'm encountering a puzzling issue that has me feeling like I know the solution, yet I don't. I set "State" to [checked]. The problem arises when, upon turning it into a map and clicking just one checkbox, the entire selection is clicked. To addre ...

Switch the language setting of "Timeagopipe" in an Ionic 2 application

Greetings everyone, I am currently attempting to modify the displayed language of Timeagopipe on my page1.html: {{myDatet | amTimeAgo}} Currently, it displays: 4 days ago Is there a way for me to switch it to a different language other than English? I ...

Issue with Symbol Constructor in Typescript: [ts] The 'new' keyword can only be used with a void function

Just starting out with typescript and experimenting with the ES6 Symbol constructor. How can I address this ts lint problem without resorting to using any? const symbol = new Symbol(path); I'm trying to avoid doing this: const symbo ...

The art of expanding Angular's HTTP client functionality

I understand that the following code is not valid in Angular, but I am using it for visual demonstration purposes. My goal is to enhance the Angular HTTP client by adding custom headers. I envision creating a class like this, where I extend the Angular h ...