Form a fixed array category based on an object class

I have a structure similar to the following:

type Fields = {
  countryCode: string;
  currency: string;
  otherFields: string;
};

Additionally, I possess an immutable array that looks like this:

// Type: readonly ["countryCode", "currency", "otherFields"]
const allowedFields = ["countryCode", "currency", "otherFields"] as const;

My goal is to define an interface for this array declaration based on the `Fields` object type in order to enforce that any modification made to it must also be reflected in the array. It should work something like this:

// Creating 'SomeType'
const allowedFields: SomeType = ["countryCode"] as const; // This should raise an error due to missing fields

const allowedFields: SomeType = ["extraField"] as const; // This should raise an error since "extraField" is not part of the 'Fields' object type

Answer №1

type Fields = {
  countryCode: string;
  currency: string;
  otherFields: string;
};

// credits goes to https://twitter.com/WrocTypeScript/status/1306296710407352321
type TupleUnion<U extends string, R extends any[] = []> = {
  [S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];


type AllowedFields = TupleUnion<keyof Fields>;


const allowedFields: AllowedFields = ["countryCode", "currency", "otherFields"];


// How to create 'SomeType'?
const foo: AllowedFields  = ["countryCode"]; // Should throw error because there are missing fields

const bar: AllowedFields  = ["extraField"]; // Should throw error because "extraField" is not in the object type 'Fields'

Generate a permutation of all permitted properties to deal with the unordered nature of dictionary keys.

Playground

EXPLANATION

To simplify things without recursion and conditional types:

{
  type TupleUnion<U extends string, R extends any[] = []> = {
    [S in U]: [...R, S]
  }

  type AllowedFields = TupleUnion<keyof Fields>;
  type AllowedFields = {
    countryCode: ["countryCode"];
    currency: ["currency"];
    otherFields: ["otherFields"];
  }
}

We've structured an object where each value is a tuple with a key. To accomplish this, every value must contain every key in a different order. For example:

  type AllowedFields = {
    countryCode: ["countryCode", 'currency', 'otherFields'];
    currency: ["currency", 'countryCode', 'otherFields'];
    otherFields: ["otherFields", 'countryCode', 'currency'];
  }

Hence, to add two additional props, we need to recursively call TupleUnion, excluding an element already existing in the tuple. This means our second call should do this:


  type AllowedFields = {
    countryCode: ["countryCode", Exclude<Fields, 'countryCode'>];
    currency: ["currency", Exclude<Fields, 'currency'>];
    otherFields: ["otherFields", Exclude<Fields, 'otherFields'>];
  }

To achieve this, we use:

TupleUnion<Exclude<U, S>, [...R, S]>;
. It may be clearer if written as:

type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
    [Key in FieldKeys]: TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
  }

If we implemented it like this, we'd end up with deeply nested data structures:

  type AllowedFields = TupleUnion<keyof Fields>['countryCode']['currency']['otherFields']

We shouldn't recurse into TupleUnion if Exclude<U, S> (or

Exclude<FieldKeys, Key></code) results in <code>never</code. We check if <code>Key
is the last property, and if so, return just [...R,S].

This code snippet:

{
  type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
    [Key in FieldKeys]: Exclude<FieldKeys, Key> extends never ? [...Result, Key] : TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
  }

  type AllowedFields = TupleUnion<keyof Fields>

}

is more straightforward. However, we still have an object with values instead of tuples. Each value in the object represents a tuple of the desired type. To get a union of all values, we simply use square bracket notation with a union of all keys, like

type A = {age:1,name:2}['age'|'name'] // 1|2
.

Final adjusted code:

 type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
    [Key in FieldKeys]: Exclude<FieldKeys, Key> extends never ? [...Result, Key] : TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
  }[FieldKeys] // added square bracket notation with union of all keys

Answer №2

Utilizing the code shared on this post is a good way to avoid encountering the issue of

Type instantiation is excessively deep and possibly infinite (ts2589)
as mentioned in @captain-yossarian's response (example) :

type Fields = {
  countryCode: string;
  currency: string;
  otherFields: string;
};

type UnionToParm<U> = U extends any ? (k: U) => void : never;
type UnionToSect<U> = UnionToParm<U> extends (k: infer I) => void ? I : never;
type ExtractParm<F> = F extends { (a: infer A): void } ? A : never;

type SpliceOne<Union> = Exclude<Union, ExtractOne<Union>>;
type ExtractOne<Union> = ExtractParm<UnionToSect<UnionToParm<Union>>>;

type ToTuple<Union> = ToTupleRec<Union, []>;
type ToTupleRec<Union, Rslt extends any[]> = SpliceOne<Union> extends never
  ? [ExtractOne<Union>, ...Rslt]
  : ToTupleRec<SpliceOne<Union>, [ExtractOne<Union>, ...Rslt]>;

type AllowedFields = ToTuple<keyof Fields>;


const allowedFields: AllowedFields = ["countryCode", "currency", "otherFields"];


// How to create 'SomeType'?
const foo: AllowedFields  = ["countryCode"]; // Should throw error because there are missing fields

const bar: AllowedFields  = ["extraField"]; // Should throw error because "extraField" is not in the object type 'Fields'

Playground

Answer №3

This approach offers a different perspective from what you initially requested by allowing us to create an object utilizing the array as a type rather than imposing restrictions on an array based on a specific object type. The relationship between them is two-way, so there might be opportunities for utilization in both directions.

Start by generating a type from your immutable array and then establish a mapping type using it as the key type. Mistakes such as forgetting to assign one of the array values to the object or assigning one that does not exist in the array will result in an error being thrown.

const categoryNames = ['a', 'b', 'c', 'd'] as const

export type Keys = typeof categoryNames[number]

export type Categories<Key extends string, Type> = {
  [name in Key]: Type
}

const mapping: Categories<Keys, number> = {
  a: 0,
  // b: 1, with 'b' commented out there will be an error
  c: 2,
  d: 3
}

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

Unable to access the camera page following the installation of the camera plugin

The issue I'm encountering involves using the native camera with the capacitor camera plugin. After implementation, I am unable to open the page anymore - clicking the button that should route me to that page does nothing. I suspect the error lies wit ...

Detecting changes in input text with Angular and Typescript

Searching for a straightforward and practical method to identify changes in my textfield has been challenging. Avoiding the use of (keypress) is necessary, as users may occasionally paste values into the field. The (onchange) event only triggers when the u ...

Conceal a division based on its numerical position

I'm having trouble figuring out how to hide multiple divs based on an index that I receive. The code snippet below hides only the first div with the id "medicalCard", but there could be more than one div with this id. document.getElementById("medical ...

The module has been declared by multiple NgModules

After creating the ExampleComponent component and declaring it in a module that is not listed in app.module, I now want to declare the same ExampleComponent in a module that is included in app.module. How can I achieve this without encountering an error st ...

Best Practices for Organizing Imports in Typescript to Prevent Declaration Conflicts

When working with TypeScript, errors will be properly triggered if trying to execute the following: import * as path from "path" let path = path.join("a", "b", "c") The reason for this error is that it causes a conflict with the local declaration of &ap ...

Having conflicting useEffects?

I often encounter this problem. When I chain useEffects to trigger after state changes, some of the useEffects in the chain have overlapping dependencies that cause them both to be triggered simultaneously instead of sequentially following a state change. ...

What is the reason behind the absence of compile time errors when using 'string' functions on an 'any' field type variable in TypeScript?

Looking at the following typescript code snippet: let a; a = "number"; let t = a.endsWith('r'); console.log(t); It is worth noting that since variable 'a' is not declared with a specific type, the compiler infers it as ...

Guide to sending a body containing formData inside a key using the fetch API

Whenever I attempt to send an image and a path to the API, it is being sent as [object Object] export async function uploadImageToCDN(image: FormData, directory: string = 'dir'): Promise<any> { const token = await authorizeInApi() const he ...

Guide to successfully passing a function as a prop to a child component and invoking it within Vue

Is it really not recommended to pass a function as a prop to a child component in Vue? If I were to attempt this, how could I achieve it? Here is my current approach: Within my child component - <template> <b-card :style="{'overflow-y&apo ...

unable to access environment file

Recently, I delved into the world of TypeScript and created a simple mailer application. However, I encountered an issue where TypeScript was unable to read a file. Placing it in the src folder did not result in it being copied to dist during build. When I ...

Experimenting with a module reliant on two distinct services

I am facing an issue with a component that relies on a service to fetch data. The service also retrieves configurations from a static variable in the Configuration Service, but during Karma tests, the const variable is showing up as undefined. Although I ...

Tips on customizing the Nuxt Route Middleware using Typescript

I am working on creating a route middleware in TypeScript that will validate the request.meta.auth field from the request object. I want to ensure this field has autocomplete options of 'user' and 'guest': export default defineNuxtRoute ...

When attempting to navigate to the index page in Angular, I encounter difficulties when using the back button

I recently encountered an issue with my Angular project. On the main index page, I have buttons that direct me to another page. However, when I try to navigate back to the index page by clicking the browser's back button, I only see a white page inste ...

There is a type error in the dynamic assignment in TypeScript. I have various data that need to be fetched from one another

const obj1 = { a: 1, b: "xx" }; const obj2 = { a: 2, b: "3", d: "hello" }; for (const key in obj1) { const _key = key as keyof typeof obj1; obj1[_key] = obj2[_key]; } x[_key] error Type 'string | number' is no ...

A TypeScript function that converts a value into an array if it is not already an array, ensuring the correct type is output

I'm attempting to develop a function that wraps a value in an array if it is not already an array. export function asArray<T extends Array<any>>(value: T): T export function asArray<T>(value: T): T[] export function asArray(value: a ...

Tips for creating a nodeCategoryProperty function for a TypeScript Model:

nodeCategoryProperty function has a signature requirement of (a: ObjectData, b?: string) => string. In my opinion, this should be updated to (a: ObjectData, b?: string) => string | void, as the function is intended to not return anything if used as a ...

Firebase Angular encountering issues with AngularFirestoreModule

I have encountered a challenge with Firebase authentication in my Angular applications. Due to updated read and write rules that require auth!=null, I successfully implemented Firebase authentication in one of my apps using Angular 13. Now, I am trying to ...

Issue with Angular 12.1: Unable to retrieve value using "$event.target.value"

I am just starting to dive into the world of Angular and have been using YouTube tutorials as my guide. However, I have encountered an error in one of the examples provided. Below is the code snippet that is causing me trouble. HTML <input type=" ...

encountering the issue of not being able to assign a parameter of type 'string | undefined' to a parameter of type

Seeking help with the following issue: "Argument of type 'string | undefined' is not assignable to parameter of type" I am unsure how to resolve this error. Here is the section of code where it occurs: export interface IDropDown { l ...

Support for dark mode in Svelte with Typescript and TailwindCSS is now available

I'm currently working on a Svelte3 project and I'm struggling to enable DarkMode support with TailwindCSS. According to the documentation, it should be working locally? The project is pretty standard at the moment, with Tailwind, Typescript, and ...