Design a unique TypeScript index type with properties `[key of Y]: Partial<X>` that, when paired with a `default: Partial<X>` property, do not remain partial themselves

Here are some TypeScript type definitions to consider:

enum Environment {
  Local = 'local',
  Prod = 'prod'
}

type EnvironmentConfig = {
  isCustomerFacing: boolean,
  serverUrl: string
}

type DefaultBaseConfig<T> = {
  default: T
}

type EnvironmentBaseConfig<T> = {
  [key in Environment]: T
}

type BaseConfig<T> = DefaultBaseConfig<T> | EnvironmentBaseConfig<T>;

// const baseConfig: ??? = {
const baseConfig: BaseConfig<Partial<EnvironmentConfig>> = {
  default: {
    isCustomerFacing: false
  },
  local: {
    serverUrl: 'https://local.example.com'
  },
  prod: {
    isCustomerFacing: true
  }
};

There's an object called baseConfig, but it needs refinement. I want each environment-keyed property to be a Partial<EnvironmentConfig>, and the default property to also require that when combined with any environment, they must form a full EnvironmentConfig.

In this example, local works because its properties align with default. However, prod doesn't work because it lacks the necessary serverUrl property.

This config object will later be merged conditionally using TypeScript constraints to ensure runtime functionality. I've tried various approaches, like conditional types, but haven't found a solution yet.

Is there a way to achieve this goal effectively?

Below is my current attempt, which hasn't yielded the desired results:

type SplitWithDefault<
  TComplete,
  TDefault extends Partial<TComplete>,
  TSplit extends { default: TDefault, [key: string]: Partial<TComplete> }
> = { default: TDefault }
  & { [P in keyof Omit<TSplit, 'default'>]: (TSplit[P] & TDefault) extends TComplete ? TSplit[P] : never };

Answer №1

Your specified BaseConfig is more of a self-referential generic constraint rather than a distinct type in TypeScript. Essentially, when given a particular candidate type T, you can validate if it conforms to a BaseConfigConstraint<T> rule, but defining "all types that meet this rule" as a single TypeScript object type may prove difficult or impossible.

In situations like this, I typically create a helper identity function that takes a single argument and only accepts arguments of type

T extends BaseConfigConstraint<T>
for an appropriate definition of BaseConfigConstraint<T>. Something like the following:

const asBaseConfig = <T extends
  { default: Partial<EnvironmentConfig> } &
  Record<Environment,
    Partial<EnvironmentConfig> &
    Omit<EnvironmentConfig, keyof T['default']>
  >
>(baseConfig: T) => baseConfig;

This snippet specifies that baseConfig must be of type T with:

  • a default property of a subtype of Partial<EnvironmentConfig>, and
  • properties at Environment keys consist of subtypes of both:
    • Partial<EnvironmentConfig>, and
    • an object containing any properties of EnvironmentConfig that are missing from the default property of T.

In simpler terms, T must include keys from Environment and "default", all of which must have properties of type

Partial<EnvironmentConfig></code. Additionally, any properties absent from the <code>default
property must exist in the Environment ones.

Lets test this on your example:

const baseConfig = asBaseConfig({
  default: {
    isCustomerFacing: false
  },
  local: {
    serverUrl: 'https://local.example.com'
  },
  prod: { // error!
//~~~~ <-- Property 'serverUrl' is missing 
    isCustomerFacing: true,
  }
});

The encountered error aligns with your expectations. You can rectify the issue by adding serverUrl to either prod or default. So far, so good.

Note that employing a constraint instead of a type necessitates any function or type expecting a parameter or a property of type BaseConfig to now be a generic function or type with a type parameter corresponding to this constraint. This shift may impact how you structure your codebase.

Link to Playground with 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

The function of edit does not exist

I'm currently working on creating a discord bot that will send a message to a specific channel upon startup. Initially, I was able to send the message to the designated channel without any issues. However, when I attempted to edit the message, an erro ...

Adding values to Firebase Realtime Database using Angular: A step-by-step guide

I'm looking to integrate Firebase Realtime Database with Angular in order to add values. Can anyone provide guidance on how to achieve this integration? Thanks in advance! ...

What is the best way to pass a generic interface to the zustand create function in a TypeScript environment

Having trouble figuring out the right syntax to pass a generic interface when calling a function that accepts a generic type. My goal is to use: const data = itemStore<T>(state => state.data) import { create } from "zustand"; interface ...

Tips for securely encrypting passwords before adding them to a database:

While working with Nest.Js and TypeORM, I encountered an issue where I wanted to hash my password before saving it to the database. I initially attempted to use the @BeforeInsert() event decorator but ran into a roadblock. After some investigation, I disc ...

Addressing ESLint and TypeScript Issues in Vue.js with Pinia: A comprehensive guide

Experiencing difficulties with Vue.js + Pinia and need assistance to resolve these issues. Error: 'state:' is defined but never used. Here is the snippet of code located in @/stores/user.ts. import { defineStore } from 'pinia' export ...

Sort through the files for translation by utilizing a feature within Typewriter

I am looking to implement Typewriter in a project that involves translating many C# files into TypeScript using WebEssentials. Is there a way to configure the template so that only class files containing a specific attribute are translated in this mann ...

Preventing over-purchasing products by managing Knex.js inventory levels

Currently, I am in the process of developing an online store for my school's guild organization. I must admit that I lack experience working with databases and Knex.js is still a bit challenging for me. An issue arises when multiple users simultaneo ...

TS2307 error encountered in Angular 2 TypeScript due to the inability to locate a module for a private npm

I've been working on creating some components for internal company use, with the intention of sharing them through our private npm repository. However, I've hit a roadblock while trying to add these components to an app using npm and systemjs - I ...

Define the data type for the toObject function's return value

Is it possible to define the return type of the toObject method in Mongoose? When working with generics, you can set properties of a Document object returned from a Mongoose query. However, accessing getters and setters on these objects triggers various v ...

Creating a universal timer at the application level in Angular

Is there a way to implement a timer that will automatically execute the logout() function in my authentication.service at a specific time, regardless of which page I am currently on within my application? I attempted to create a timer within my Authentica ...

Using Angular and Typescript to Submit a Post Request

I am working on a basic Angular and Typescript page that consists of just one button and one text field. My goal is to send a POST request to a specific link containing the input from the text field. Here is my button HTML: <a class="button-size"> ...

Tips for using jest toHaveBeenCalled with multiple instances

Currently, I am in the process of writing a test case for one of my functions. This function calls another function from a library, and I am attempting to mock this function (saveCall). Below is a snippet of the sample code in question: import { Call } fro ...

Sharing API data between components in Angular 5

Summary: I'm trying to retrieve data from an input field in a component form, then compare it using API services. After that, I want to take the value from the "correo" field in the form and pass it to another component. In this other component, I aim ...

Using Angular2, you can dynamically assign values to data-* attributes

In my project, I am looking to create a component that can display different icons based on input. The format required by the icon framework is as follows: <span class="icon icon-generic" data-icon="B"></span> The data-icon="B" attribute sp ...

Webpack bundling only a singular Typescript file rather than all of its dependencies

I'm currently facing a challenge while attempting to consolidate all the files in my Typescript project, along with their dependencies from node_modules, into a single file using Webpack. Despite trying multiple options, it seems that only the entry f ...

Error: Tried to modify a property that is read-only while using the useRef() hook in React Native with Typescript

https://i.sstatic.net/jhhAN.pngI'm facing an issue while attempting to utilize a useRef hook for a scrollview and pan gesture handler to share a common ref. Upon initializing the useRef() hook and passing it to both components, I encounter an error th ...

An error is anticipated when () is added, but surprisingly, I still encounter an error as well. This issue arises in React-Native and Typescript

I am still relatively new to React-Native, but I have been using React-Native, TypeScript, and integrating it with Firebase. An unusual error has occurred, which is visible in the screenshot below. However, when checking in VSC, the error seems to be non-e ...

Vitest's behavior shows variance when compared to compiled JavaScript

Challenge Currently, I am developing a package that relies on decorators to initialize class object properties. The specific decorator is expected to set the name property of the class. // index.ts const Property = (_target: any, key: any) => { } cl ...

Angular findIndex troubleshooting: solutions and tips

INFORMATION = { code: 'no1', name: 'Room 1', room: { id: 'num1', class: 'school 1' } }; DATABASE = [{ code: 'no1', name: 'Room 1', room: { id: 'num1', ...

Filter an item from an array of objects using its index (TypeScript and Next.js)

I am attempting to filter out a specific item from the list by using its index. For example, if I want to remove the item with index 0, I would pass the number 0 to the filter function in order to return all objects except for the one at position 0. Belo ...