Prevent identical objects from being interchangeable in Typescript

I have a situation where I frequently use a StringToString interface:

interface StringToString {
    [key: string]: string;
}

There are instances when I need to switch the keys and values in my objects. In this scenario, the keys become values and the values become keys. Here is the type signature for the function:

function inverse(o: StringToString): StringToString;

The issue arises from the fact that I often perform this key-value exchange and I want to determine if the object being manipulated has keys as values or keys as keys.

To address this requirement, I defined two types:

export interface KeyToValue {
    [key: string]: string;
}
export interface ValueToKey {
    [value: string]: string;
}
type StringToString = KeyToValue | ValueToKey;

This leads to a modified version of the inverse function:

/** Inverses a given object: keys become values and values become keys. */
export function inverse(obj: KeyToValue): ValueToKey;
export function inverse(obj: ValueToKey): KeyToValue;
export function inverse(obj: StringToString): StringToString {
    // Implementation
}

My goal now is to have TypeScript generate errors when attempting to assign ValueToKey to KeyToValue. For instance, this assignment should produce an error:

function foo(obj: KeyToValue) { /* ... */ }

const bar: ValueToKey = { /* ... */ };

foo(bar) // <-- THIS SHOULD throw an error

Is there a way to achieve this?

Answer №1

Using branding concept can be beneficial in this scenario. By adding a property that is not actually present, except on the type, and utilizing functions to convert values to the correct brand type.

In typical cases, you might employ a _brand: 'Something' property, but if you need to accommodate all string keys, using a Symbol would be more suitable.

const brand = Symbol('KeysOrValuesBrand')

export interface KeyToValue {
    [key: string]: string;
    [brand]: 'KeyToValue'
}
export interface ValueToKey {
    [value: string]: string;
    [brand]: 'ValueToKey'
}

type StringToString = KeyToValue | ValueToKey

With this approach, the behavior aligns with expectations:

declare function foo(obj: KeyToValue): void
declare const bar: ValueToKey
foo(bar) // <-- THIS SHOULD throw an error

const valuesA: ValueToKey = bar // works
const valuesB: ValueToKey = inverse(bar) // error
const valuesC: ValueToKey = inverse(inverse(bar)) // works

The downside is that you need a function to create these objects for you, as that's how they acquire the brand. For instance:

function makeValueToKey(input: Record<string, string>): ValueToKey {
    return input as ValueToKey
}

function makeKeyToValue(input: Record<string, string>): KeyToValue {
    return input as KeyToValue
}

declare function foo(obj: KeyToValue): void
foo({ a: 'b' }) // error
foo(makeValueToKey({ a: 'b' })) // error
foo(makeKeyToValue({ a: 'b' })) // works

This process may seem somewhat cumbersome.

Playground


That being said, veering into personal opinion, I believe type branding is essentially a workaround to mask a flawed data model. If you can let structural typing naturally function as intended, it is likely the better path forward.

It necessitates altering your data structures, but something like the following example is much simpler:

export interface KeyToValue {
    type: 'KeyToValue'
    data: Record<string, string>
}
export interface ValueToKey {
    type: 'ValueToKey'
    data: Record<string, string>
}

type StringToString = KeyToValue | ValueToKey

Playground

Answer №2

One unique aspect of TypeScript is its structural type system, where types are evaluated based on their structure rather than their names. This means that even if two interfaces have the same content but different names, they are considered identical:

export interface KeyToValue {
    [key: string]: string;
}
export interface ValueToKey {
    [value: string]: string;
}

This allows for easy interchangeability between these similar types:

const x: KeyToValue = {myKey: myValue};
const y: ValueToKey = x;

To create distinct types, you need to introduce differences beyond just naming. For example, using different types for keys and values can help differentiate them:

type Key = "red" | "green" | "blue";
type Value = string;

You can then define:

type KeyToValue = Record<Key, Value>;
type ValueToKey = Record<Value, Key>;

The compiler will be able to distinguish between these types effectively.

If the mapping is known at compile time, you can use keyof to derive types from the provided values easily:

const colorMap = {
    red: 1,
    green: 2,
    blue: 3,
};

type Key = keyof typeof colorMap; // avoids duplicating the possible keys

Answer №3

In TypeScript, achieving this is not feasible.

Using what TypeScript refers to as a Structural Typing system (which is a subset of "Duck Typing"), the name of a type is disregarded and only the structure matters. As stated in the handbook:

The guiding principle of TypeScript's structural type system is that x is considered compatible with y if y has at least the same members as x.

Since all your interfaces share the same definition (not taking into account index names, which are irrelevant in this context), they can be freely exchanged within the TypeScript environment.

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

Input values that are true, or in other words, fulfill conditions for truthiness

Is there a specific data type in TypeScript to represent truthy values? Here's the method I'm working with: Object.keys(lck.lockholders).length; enqueue(k: any, obj?: any): void It seems like TypeScript allows checking for empty strings &ap ...

What is the best way to reload a React/TypeScript page after submitting a POST request?

I am working on a custom plugin for Backstage that interacts with Argo CD via API calls. To retrieve application information, I make a GET request to the following endpoint: https://argocd.acme.com/api/v1/applications/${app-name} If the synchronizati ...

Can you explain the meaning of the code snippet: ` <TFunction extends Function>(target: TFunction) => TFunction | void`?

As I delve into the contents of the lib.d.ts file, one particular section caught my attention: declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; The syntax in this snippet is a bit perplexing to m ...

Setting up the TypeScript compiler locally in the package.json file

UPDATE #1: It appears that I have managed to come up with a functional configuration, but I am open to any suggestions for improvement. Feel free to check out the answer here: THE ORIGINAL INQUIRY: I am in the process of setting up my environment so that ...

Using various types in TypeScript to represent a variety of events

If I have these three distinct objects: { id: 1, time: 1000, type: 'A', data: { a: 'one' } } { id: 2, time: 1001, type: 'B', data: { b: 123 } } { id: 3, time: 1002, type: 'C', data: { c: 'thre ...

Having trouble understanding how to receive a response from an AJAX request

Here is the code that I am having an issue with: render() { var urlstr : string = 'http://localhost:8081/dashboard2/sustain-master/resources/data/search_energy_performance_by_region.php'; urlstr = urlstr + "?division=sdsdfdsf"; urlst ...

Can anyone provide guidance on setting up a TypeScript service worker in Vue 3 using the vite-plugin-pwa extension?

I am looking to develop a single-page application that can be accessed offline. To achieve this, I have decided to implement a PWA Service Worker in my Vue webapp using TypeScript and Workbox. I found useful examples and guidance on how to do this at . Ho ...

What could be preventing the nesting of observables from functioning properly in Ionic-Angular?

Working with Observables has been an interesting experiment for me, but I'm facing an issue that I can't seem to resolve. While all the methods work perfectly fine when called outside the pipe, the problem arises when I nest them like this: creat ...

What is the best way to store values in a map for future reference within a Kotlin class?

Looking to implement a map of key value pairs in Kotlin inside a class that is mutable and can be updated and referenced as needed. Research suggests that using a MutableMap would be the appropriate choice, given its ability to be updated at any point. I ...

Guide to managing MUI's theme typography font weight choices with TypeScript

I am interested in learning how to incorporate a new font weight into my theme, for example, fontWeightSemiBold, and disable the existing fontWeightLight and fontWeightMedium. I believe this can be achieved through module augmentation. For reference, there ...

What is the reason behind tsc (Typescript Compiler) disregarding RxJS imports?

I have successfully set up my Angular2 project using JSPM and SystemJS. I am attempting to import RxJS and a few operators in my boot.ts file, but for some reason, my import is not being transpiled into the final boot.js output. // boot.ts import {Observa ...

How do I set up middleware with async/await in NestJS?

I am currently integrating bull-arena into my NestJS application. export class AppModule { configure(consumer: MiddlewareConsumer) { const queues = this.createArenaQueues(); const arena = Arena({ queues }, { disableListen: true }); consumer. ...

Angular modal not responding to close event

My issue is that when I try to close a modal by pressing the X button, it doesn't work. Here is the button where I am triggering the modal: <button type="submit" id="submit-form" class="btn btn-primary" (click)="o ...

Creating an object type that includes boolean values, ensuring that at least one of them is true

To ensure both AgeDivisions and EventStyles have at least one true value, I need to create a unique type for each. These are the types: type AgeDivisions = { youth: boolean; middleSchool: boolean; highSchool: boolean; college: boolean; open: bo ...

"Error in Visual Studio: Identical global identifier found in Typescript code

I'm in the process of setting up a visual studio solution using angular 2. Initially, I'm creating the basic program outlined in this tutorial: https://angular.io/docs/ts/latest/guide/setup.html These are the three TS files that have been genera ...

Generics in Classes: Unintelligible Error

For a demonstration, you can check out the implementation in this codesanbox. The class "Operation.ts" contains all the details. The purpose of the "Operation" class is to manage operations performed on objects such as rectangles. Each operation type ("mo ...

The material UI style is not being implemented properly in the final production or build

While applying styles to the ListItemButton component from MUI by targeting the specific class .css-10hburv-MuiTypography-root, it works fine in development but not in production. I have tried various methods, including directly applying the styles on th ...

How can I leverage the data fetched from API in Angular 13?

Just dipping my toes into the world of Angular by creating a quiz app to gain some hands-on experience. Successfully receiving a random set of questions from the API, but now facing the challenge of iterating over this array to implement the gameplay. The ...

Removing undefined elements from an array

Could somebody clarify why, in this particular scenario: const dataValues: ValueRange[] = res.data.valueRanges.filter((range: ValueRange) => range.values); const formattedValues: Array<SheetData | undefined> = dataValues.map(this.formatSheetRang ...

The JSON creation response is not meeting the expected criteria

Hello, I'm currently working on generating JSON data and need assistance with the following code snippet: generateArray(array) { var map = {}; for(var i = 0; i < array.length; i++){ var obj = array[i]; var items = obj.items; ...