Reassigning Key Names and Types Based on Conditions

How can I modify object key names and properties in a way that allows existing keys and properties to remain the same or be modified (remapped)? My current approach does not properly handle mixed cases:


export const FUNC_ENDING_HINT = "$func" as const;
type DataOrFunc<TD, K extends keyof TD> = Record<`${K}.${typeof FUNC_ENDING_HINT}`, Record<string, any[]>> | Record<K, TD[K]>;
export type DataOrFuncFromObject<TD> = DataOrFunc<TD, keyof TD>;
type Schema = {
  col1: string;
  col2: string;
};

const basic: DataOrFuncFromObject<Schema> = {
  col1: "",
  col2: ""
}
//@ts-expect-error
const incompleteBasic: DataOrFuncFromObject<Schema> = {
  col1: "",
}
//@ts-expect-error
const incompleteFunc: DataOrFuncFromObject<Schema> = {
  "col2.$func": { func: [] },
}
const funcs: DataOrFuncFromObject<Schema> = {
  "col1.$func": { func: [] },
  "col2.$func": { func: [] },
}
const mixed: DataOrFuncFromObject<Schema> = {
  col1: "",
  "col2.$func": { func: [] },
}

Playground

Answer №1

Alright, I believe I've cracked it, although the current solution may not be aesthetically pleasing and could use some enhancements. Essentially, I've utilized a mapped type named DataOrFuncValuesObject. This type maps the target type (Schema in this case) to an object type where the keys remain the same, but the value types are categorized as a union of two objects: one in its original form (col1: string) and another in a function form (

"$col1.$func": Record<string, any[]>
). Subsequently, I leverage a type inspired by an existing answer, which I've dubbed PropertyValueIntersection to extract these property values as an intersection. So, given:

type Schema = {
    col1: string;
    col2: string;
};

The result is as follows:

type Result =
   & ({ col1: string} | {$"col1.$func": Record<string, any[]>})
   & ({ col2: string} | {$"col2.$func": Record<string, any[]>});

Here's how it looks:

export const FUNC_ENDING_HINT = "$func" as const;

// Symbol conversion to never (refer to next type)
type NonSymbol<K extends PropertyKey> = K extends Symbol ? never : K;

// Transforming the provided object into another object featuring properties with types
// that are unions of the key and its corresponding value or the mapped key along with
// the 'Record<string, any[]>' type.
// Note: The 'NonSymbol' piece is only necessary if your configuration doesn't exclusively support strings as keys.
type DataOrFuncValuesObject<ObjectType> = {
    [Key in keyof ObjectType]:
        | { [K in Key]: ObjectType[Key] }
        | { [K in `${NonSymbol<Key>}.${typeof FUNC_ENDING_HINT}`]: Record<string, any[]> };
};

// Derive the property types of each property within the given object as an intersection
// (Referenced from https://stackoverflow.com/a/66445507/157247).
// It was titled 'ParamIntersection' there and wasn't generic; however, that's where I obtained it.
type PropertyValueIntersection<O> = {
    [K in keyof O]: (x: O[K]) => void;
}[keyof O] extends (x: infer I) => void
    ? I
    : never;

// Our customized type mapping
type DataOrFuncFromObject<TD> = PropertyValueIntersection<DataOrFuncValuesObject<TD>>;

I find the 'NonSymbol' element within the template literal of 'DataOrFuncValuesObject' somewhat hacky. My belief is that I should be able to exclude 'symbol' from the potential types of 'Key' earlier on, but my endeavors to achieve that have been unsuccessful.

Application/tests:

type Schema = {
    col1: string;
    col2: string;
};

// Passes
const basic: DataOrFuncFromObject<Schema> = {
    col1: "",
    col2: "",
};
// Succeeds
const funcs: DataOrFuncFromObject<Schema> = {
    "col1.$func": { func: [] },
    "col2.$func": { func: [] },
};
// Valid
const mixed: DataOrFuncFromObject<Schema> = {
    col1: "",
    "col2.$func": { func: [] },
};
// Throws an error due to missing `col1` / `"col1.$func"`
const wrong: DataOrFuncFromObject<Schema> = {
    "col2.$func": { func: [] },
};

confirmed in a comment that was acceptable (though suboptimal). Should you wish to prohibit such occurrences, we can modify DataOrFuncValuesObject by incorporating the "other" property in each object type within the union as an optional attribute with the type never:

// Converts the given object into an object where each property's type is
// a union of the key and its value type or the mapped key and the
// `Record<string, any[]>` type.
// Note: You only need the `NonSymbol` thing below if your configuration
// has `keyofStringsOnly` turned off.
type DataOrFuncValuesObject<ObjectType> = {
    [Key in keyof ObjectType]:
        | ({ [K in Key]: ObjectType[Key] } & {
              [K in `${NonSymbol<Key>}.${typeof FUNC_ENDING_HINT}`]?: never;
          })
        | ({ [K in `${NonSymbol<Key>}.${FUNC_ENDING_HINT}`]: Record<string, any[]> } & {
              [K in Key]?: never;
          });
};

Consequently, the subsequent time:

// ERROR, both 'col1' and '"col1.$func"' cannot coexist
const wrong2: DataOrFuncFromObject<Schema> = {
    col1: "",
    "col1.$func": { func: [] },
    "col2.$func": { func: [] },
};

Okay, I think I've got it, but it's ugly and I bet it can be improved. I've used a mapped type called DataOrFuncValuesObject that maps the target type (Schema in the example) to an object type where the keys are the same but the value types are a union of objects, one in the original form (col1: string) and one in the function form (

"$col1.$func": Record<string, any[]>
). Then I use a type inspired by this answer which I've called PropertyValueIntersection to extract those property values as an intersection. So given:

type Schema = {
    col1: string;
    col2: string;
};

...the result is:

type Result =
   & ({ col1: string} | {$"col1.$func": Record<string, any[]>})
   & ({ col2: string} | {$"col2.$func": Record<string, any[]>});

Here it is:

export const FUNC_ENDING_HINT = "$func" as const;

// Converts Symbol to never (see next type)
type NonSymbol<K extends PropertyKey> = K extends Symbol ? never : K;

// Converts the given object into an object where each property's type is
// a union of the key and its value type or the mapped key and the
// `Record<string, any[]>` type.
// Note: You only need the `NonSymbol` thing below if your configuration
// has `keyofStringsOnly` turned off.
type DataOrFuncValuesObject<ObjectType> = {
    [Key in keyof ObjectType]:
        | { [K in Key]: ObjectType[Key] }
        | { [K in `${NonSymbol<Key>}.${typeof FUNC_ENDING_HINT}`]: Record<string, any[]> };
};

// Extract the property types of the properties in the given object
// as an intersection; see https://stackoverflow.com/a/66445507/157247
// (It's called `ParamIntersection` there and isn't generic, but that's
// where I got this from.)
type PropertyValueIntersection<O> = {
    [K in keyof O]: (x: O[K]) => void;
}[keyof O] extends (x: infer I) => void
    ? I
    : never;

// Our mapped type
type DataOrFuncFromObject<TD> = PropertyValueIntersection<DataOrFuncValuesObject<TD>>;

The NonSymbol thing in the template literal in DataOrFuncValuesObject feels like a hack, it seems to me I should be able exclude symbol from the possible types of Key earlier, but my attempts to do that failed.

Usage/tests:

type Schema = {
    col1: string;
    col2: string;
};

// OK
const basic: DataOrFuncFromObject<Schema> = {
    col1: "",
    col2: "",
};
// OK
const funcs: DataOrFuncFromObject<Schema> = {
    "col1.$func": { func: [] },
    "col2.$func": { func: [] },
};
// OK
const mixed: DataOrFuncFromObject<Schema> = {
    col1: "",
    "col2.$func": { func: [] },
};
// ERROR, missing `col1` / `"col1.$func"
const wrong: DataOrFuncFromObject<Schema> = {
    "col2.$func": { func: [] },
};

Playground link

That version does allow both col1 and "col1.$func" in the same object, which you confirmed in a comment was okay (though not ideal). But if you want to disallow that, we can do that by modifying DataOrFuncValuesObject to include the "other" property in each object type in the union as an optional property with the type never:

// Converts the given object into an object where each property's type is
// a union of the key and its value type or the mapped key and the
// `Record<string, any[]>` type.
// Note: You only need the `NonSymbol` thing below if your configuration
// has `keyofStringsOnly` turned off.
type DataOrFuncValuesObject<ObjectType> = {
    [Key in keyof ObjectType]:
        | ({ [K in Key]: ObjectType[Key] } & {
              [K in `${NonSymbol<Key>}.${typeof FUNC_ENDING_HINT}`]?: never;
          })
        | ({ [K in `${NonSymbol<Key>}.${typeof FUNC_ENDING_HINT}`]: Record<string, any[]> } & {
              [K in Key]?: never;
          });
};

Then this test happily fails:

// ERROR, can't have both `col1` and `"col1.$func"`
const wrong2: DataOrFuncFromObject<Schema> = {
    col1: "",
    "col1.$func": { func: [] },
    "col2.$func": { func: [] },
};

Playground link

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

How to retrieve a value from an Angular form control in an HTML file

I have a button that toggles between map view and list view <ion-content> <ion-segment #viewController (ionChange)="changeViewState($event)"> <ion-segment-button value="map"> <ion-label>Map</ion-label> & ...

"Discover the power of Next.js by utilizing dynamic routes from a curated list

I am working on a Next.js application that has a specific pages structure. My goal is to add a prefix to all routes, with the condition that the prefix must be either 'A', 'B', or 'C'. If any other prefix is used, it should re ...

Vue.js and TypeScript combination may result in a 'null' value when using file input

I am attempting to detect an event once a file has been uploaded using a file input. Here is the JavaScript code: fileSelected(e: Event) { if ((<HTMLInputElement>e.target).files !== null && (<HTMLInputElement>e.target).files[0] !== null) { ...

Best Practices for Implementing Redux Prop Types in Typescript React Components to Eliminate TypeScript Warnings

Suppose you have a React component: interface Chat { someId: string; } export const Chat = (props: Chat) => {} and someId is defined in your mapStateToProps: function mapStateToProps(state: State) { return { someId: state.someId || '' ...

Using NextJS to execute a Typescript script on the server

I am working on a NextJS/Typescript project where I need to implement a CLI script for processing files on the server. However, I am facing difficulties in getting the script to run successfully. Here is an example of the script src/cli.ts: console.log(" ...

How can I store an array of objects in a Couchbase database for a specific item without compromising the existing data?

Here is an example: { id:1, name:john, role:[ {name:boss, type:xyz}, {name:waiter, type:abc} ] } I am looking to append an array of objects to the existing "role" field without losing any other data. The new data should be added as individual ob ...

Navigating onRelease event with Ionic2 components - a user's guide

I'm currently working on creating a button functionality similar to the voice note feature in WhatsApp. The idea is that when the user holds down the button, the voice recording starts, and upon releasing the button, any action can be performed. Whil ...

CompositeAPI: Referencing HTML Object Template - Error TS2339 and TS2533 when using .value to access Proxy Object

Having trouble referencing an element in VueJS 3 CompositeAPI. In my current implementation, it looks like this: <div ref="myIdentifier"></div> setup() { const myIdentifier = ref(null); onMounted(() => { console.log(myIden ...

Are the missing attributes the type of properties that are absent?

I have a pair of interfaces: meal-component.ts: export interface MealComponent { componentId: string; componentQuantity: number; } meal.ts: import { MealComponent } from 'src/app/interfaces/meal-component'; export interface Meal { ...

Can I exclusively utilize named exports in a NextJS project?

Heads up: This is not a repeat of the issue raised on The default export is not a React Component in page: "/" NextJS I'm specifically seeking help with named exports! I am aware that I could switch to using default exports. In my NextJS ap ...

Converting JQueryPromise to Promise: A step-by-step guide

In my current project, there is a code snippet that produces a JQuery promise: const jqProm = server.downloadAsync(); I am interested in integrating this promise within an async function. I was thinking of creating something similar to the C# TaskComplet ...

Switch on a single component of map array utilizing react and typescript

I am currently working on mapping a reviews API's array and I encountered an issue where all the reviews expand when I click on "read more" instead of just showing the clicked review. Since I am new to TypeScript, I'm not sure how to pass the ind ...

What is the process of invoking an external JavaScript function in Angular 5?

I recently downloaded a theme from this source. I need to specify script and CSS in the index.html file. The body section of index.html looks like this: <body> <app-root></app-root> <script type="text/javascript" src="./assets/js ...

Need for utilizing a decorator when implementing an interface

I am interested in implementing a rule that mandates certain members of a typescript interface to have decorators in their implementation. Below is an example of the interface I have: export interface InjectComponentDef<TComponent> { // TODO: How ...

changing an array into json format using TypeScript

Looking to convert an array into JSON using TypeScript. How can I achieve the desired result shown below? let array = ['element1', 'element2', 'element3'] result = [{"value": "element1"}, {"value": "element2"}, {"value": "el ...

How to immediately set focus on a form control in Angular Material without needing a click event

Currently working with Angular 9 and Material, we have implemented a Stepper to assist users in navigating through our workflow steps. Our goal is to enable users to go through these steps using only the keyboard, without relying on mouse clicks for contro ...

Ways to make an element disappear when clicking outside of it in Angular 7 or with CSS

After entering text into an input field and pressing the space key, a div called 'showit' will be displayed. However, I want this div to hide when clicking outside of it. See the code below for reference: home.component.html <input type="tex ...

deleting the existing marker before placing a new marker on the Mapbox

Upon the map loading with GeoJson data, I have implemented code to display markers at specified locations. It works flawlessly, but I am seeking a way to remove previous markers when new ones are added. What adjustments should be made for this desired func ...

"Looking to log in with NextAuth's Google Login but can't find the Client Secret

I am attempting to create a login feature using Next Auth. All the necessary access data has been provided in a .env.local file. Below are the details: GOOGLE_CLIENT_ID=[this information should remain private].apps.googleusercontent.com GOOGLE_CLIENT_SECR ...

What causes different errors to occur in TypeScript even when the codes look alike?

type Convert<T> = { [P in keyof T]: T[P] extends string ? number : T[P] } function customTest<T, R extends Convert<T>>(target: T): R { return target as any } interface Foo { x: number y: (_: any) => void } const foo: Foo = c ...