Describing an Object with some typed properties

Is there a method to specify only a portion of the object type, while allowing the rest to be of any type? The primary objective is to have support for intelliSense for the specified part, with the added bonus of type-checking support. To demonstrate, let's start by defining a helper type:

type TupleToIntersection<T extends any[]> = {
  [I in keyof T]: (x: T[I]) => void
}[number] extends (x: infer U) => void
  ? U
  : never

This type does what its name suggests. Now, let's address the issue at hand.

type A = {
  A: { name: "Foo" }
}
type B = {
  B: { name: "Bar" }
}
    
type Collection<T extends any[]> = TupleToIntersection<{ [I in keyof T]: T[I] }>

declare const C: Collection<[A, B]>

C.A.name // "Foo" as expected
C.B.name // "Bar" as expected

declare const D: Collection<[A, any]>

D // <=== now of any type since an intersection of any and type A results in any
D.A.name // <=== While technically valid, there is no "intelliSense" support here

Is there a way to accomplish this?

One approach could involve keeping the type as "any" and using a typeguard to coerce it into a known shape where needed in code. This aligns with maintaining "typesafe" code according to TypeScript practices, but it may entail defining unnecessary typeguards and types manually instead of relying on automatic system definitions.

Answer №1

The concept of intersection in programming is similar to the mathematical set intersection, where the result of A & B consists of all elements that are common in both sets A and B.

any represents a set containing every possible value, so performing an intersection with any will always yield any as the final result:

// any
type Case1 = number & any;
// any
type Case2 = {a: string} & any;

To improve this situation, you can replace occurrences of any with unknown. Although similar to any, using unknown will prioritize the type from the other set:

// number
type Case1 = number & unknown
// {a: string}
type Case2 = {a: string} & unknown;

Example:

declare const D: Collection<[A, unknown]>;

D; // A
D.A.name // "Foo"

If there's a need to support additional fields being added to a type, it's recommended to use something more specific than any or unknown. For instance, to allow for new fields in an object, one can use Record<string, any>

Example:

declare const D: Collection<[A, Record<string, any>]>;

D; // A & Record<string, any
D.A.name; // "Foo"
D.additional = ''; // no error

Answer №2

Is this method suitable for your needs?

/**
 * TypeScript sometimes doesn't reduce string unions to just `string` when using `(string & {})`. This workaround is a bit unconventional and may be subject to breakage in future TypeScript versions.
 */
type StringWithAutocomplete<U extends string> = U | (string & {});

/**
 * This type defines the intellisense, excluding generic `any` properties
 */
type BaseType = {
  A: number;
  B: string;
  C: boolean;
}

/**
 * For a given key, retrieve its type in `BaseType` or default to `any`
 */
type KeyTypeOrAny<K extends StringWithAutocomplete<keyof BaseType>> =
  K extends keyof BaseType ?
    BaseType[K] :
    any;

/**
 * A mapped type that is effectively an intersection of `BaseType` and
 * `Record<string, any>`, with type-checking on defined types and
 * intellisense.
 */
type AnyWithAutocomplete = {
  [key in StringWithAutocomplete<keyof BaseType>]: KeyTypeOrAny<key>;
};

const foo: AnyWithAutocomplete = {
  A: 3,
  B: 'string',
  C: true,
  D: 'undefined types can be anything'
};

Try it out in TypeScript Playground

As a word of caution, be cautious about allowing any into your codebase. Code involving any essentially bypasses TypeScript's type checking, so it's best avoided whenever feasible.


As mentioned in the code comments, this approach relies on a somewhat unconventional method in TypeScript to permit arbitrary strings while still maintaining autocomplete functionality.

Typically, when you have a union of strings including string, like 'A' | 'B' | string, TypeScript simplifies it to just string. However, by altering the union to 'A' | 'B' | (string & {}), TypeScript retains the individual strings without collapsing the union to just string.

The {} type encompasses everything except for null or

undefined</code. Essentially, it includes all values in JavaScript where properties can be accessed without errors - exceptions being <code>null
and undefined. The built-in utility NonNullable type utilizes an intersection with this {} type, which is why it's often discouraged by linters as it might seem to imply "any object".

To create a type that combines specific properties with an open-ended property, I've taken advantage of this quirk to craft a mapped type. By associating named properties with defined types and defaulting to any for other properties, this results in a type that intersects a well-defined object type with Record<string, any> without reducing the intersection to simply any. This way, you maintain type checks and autocomplete while permitting open-ended types.

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

Tips for implementing Material-UI components in a .ts file

I am currently working on some .ts files for mocks, and I have a question about inserting MUI elements such as the Facebook icon. export const links: Link[] = [ { url: "https://uk-ua.facebook.com/", **icon: <Facebook fontSize ...

Dealing with implicit `any` when looping through keys of nested objects

Here is a simplified example of the issue I am facing: const testCase = {a:{b:"result"}} for (const i in testCase) { console.log("i", i) for (const j in testCase[i]){ console.log("j", j) } } Encountering ...

component is receiving an incompatible argument in its props

I am facing a situation where I have a component that works with a list of items, each with an ID, and a filtering function. The generic type for the items includes an ID property that all items share. Specific types of items may have additional properti ...

Tips for determining the overall percentage breakdown of 100% based on the individual denominator for every column within angular 8

In my code, I have a simple function that calculates the sum of numbers and strings in columns within a table. The sum calculation itself works fine and provides accurate results. However, the problem arises when I attempt to divide the total sum of each c ...

Assistance for Angular 2 Style Guide within Atom: Feature Needed!

My manager uses Atom with a set of eight plugins specifically designed for Angular development. I have the same plugins installed on my system, but I seem to be missing a feature that he has. We're unsure which plugin or preference setting is required ...

Is it possible to optimize the performance of my React and TypeScript project with the help of webpack?

I am working on a massive project that takes 6 to 8 minutes to load when I run npm start. Is there a way to speed up the loading process by first displaying the sign-in page and then loading everything else? ...

How do I add a new item to an object using Ionic 2?

example item: this.advData = { 'title': this.addAdvS2.value.title , 'breadcrumb': this.suggestData.breadcrumb, 'price': this.addAdvS2.value.price ...

The 'connectedCallback' property is not found in the 'HTMLElement' type

After taking a break from my project for a year, I came back to find that certain code which used to work is now causing issues: interface HTMLElement { attributeChangedCallback(attributeName: string, oldValue: string, newValue: string): void; con ...

Angular's getter value triggers the ExpressionChangedAfterItHasBeenCheckedError

I'm encountering the ExpressionChangedAfterItHasBeenCheckedError due to my getter function, selectedRows, in my component. public get selectedRows() { if (this.gridApi) { return this.gridApi.getSelectedRows(); } else { return null; } } ...

Typescript is throwing a fit because it doesn't like the type being used

Here is a snippet of code that I am working with: import { GraphQLNonNull, GraphQLString, GraphQLList, GraphQLInt } from 'graphql'; import systemType from './type'; import { resolver } from 'graphql-sequelize'; let a = ({Sy ...

Error: Unable to access the 'filter' property as it is undefined. TypeError occurred

findLoads(){ if(this.loggedInUser.userFullySetupFlag === 0 || this.loggedInUser.businessFullySetupFlag === 0){ swal( 'Incomplete Profile', 'To find loads and bid, all the details inside User Profile (My Profile) and Business Profil ...

Plugin for managing network connectivity in Ionic framework

In order to check if internet and id connection are available, I need to make a server request. I have implemented the Ionic Native Network Plugin following their official documentation. Here is my code snippet: import { Component } from '@angular/c ...

Extracting and retrieving the value from the paramMap in Angular/JavaScript

How can we extract only the value from the router param map? Currently, the output is: authkey:af408c30-d212-4efe-933d-54606709fa32 I am interested in obtaining just the random "af408c30-d212-4efe-933d-54606709fa32" without the key "authke ...

The MUI datagrid fails to display any rows even though there are clearly rows present

Today, I encountered an issue with the datagrid in Material UI. Despite having rows of data, they are not displaying properly on the screen. This problem is completely new to me as everything was working perfectly just yesterday. The only thing I did betwe ...

Utilizing the useRef hook in React to retrieve elements for seamless page transitions with react-scroll

I've been working on creating a single-page website with a customized navbar using React instead of native JavaScript, similar to the example I found in this reference. My current setup includes a NavBar.tsx and App.tsx. The NavBar.tsx file consists o ...

"What is the best way to access and extract data from a nested json file on an

I've been struggling with this issue for weeks, scouring the Internet for a solution without success. How can I extract and display the year name and course name from my .json file? Do I need to link career.id and year.id to display career year cours ...

Guide on importing absolute paths in a @nrwl/nx monorepo

I am currently working on a @nrwl/nx monorepo and I am looking to import folders within the project src using absolute paths. I attempted to specify the baseUrl but had no success. The only solution that did work was adding the path to the monorepo root ts ...

Include additional information beyond just the user's name, profile picture, and identification number in the NextAuth session

In my Next.js project, I have successfully integrated next-auth and now have access to a JWT token and session object: export const { signIn, signOut, auth } = NextAuth({ ...authConfig, providers: [ CredentialsProvider({ async authorize(crede ...

Having trouble sending a JSON object from Typescript to a Web API endpoint via POST request

When attempting to pass a JSON Object from a TypeScript POST call to a Web API method, I have encountered an issue. Fiddler indicates that the object has been successfully converted into JSON with the Content-Type set as 'application/JSON'. Howev ...

Building a resolver to modify a DynamoDB item via AppSync using the AWS Cloud Development Kit (CDK)

After successfully creating a resolver to add an item in the table using the code provided below, I am now seeking assistance for replicating the same functionality for an update operation. const configSettingsDS = api.addDynamoDbDataSource('configSet ...