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

How to utilize TypeScript fetch in a TypeScript project running on node with Hardhat?

During a test in my hardhat project on VSCode, I encountered the need to retrieve the metadata object of my NFT from a specified URL. Initially, I assumed I would have to import fs to read the URL. However, while typing out the method, I impulsively opted ...

Tips for refreshing the service page in Ionic 2

One of my services is called "user-data", and its main function is to fetch "user data" when a request is received. I have a specific page that is responsible for displaying this "user data". Upon loading the page, it retrieves the data and displays it. ...

What is the importance of using a server to host an Angular 2 app?

I recently finished developing a front-end application in Angular 2 using angular-cli. After completion, I created a bundle using the command ng build. Here's what puzzles me - my app consists only of JavaScript and HTML files. So why do I need to h ...

NuxtJS (Vue) loop displaying inaccurate information

I have a dataset that includes multiple languages and their corresponding pages. export const myData = [ { id: 1, lang: "it", items: [ { id: 1, title: "IT Page1", }, { ...

Disregard earlier callback outcome if there has been a change in the state since then

I am facing an issue with my page that displays a list of countries retrieved from an external library. When I click on a country, it should show me all the cities in that specific country. Each country object has a provided method to fetch the list of c ...

Removing a dynamic component in Angular

Utilizing Angular dynamic components, I have successfully implemented a system to display toaster notifications through the creation of dynamic components. To achieve this, I have utilized the following: - ComponentFactoryResolve - EmbeddedViewRef - Ap ...

Integrating d3.js into an Angular 2 project

Trying to incorporate the d3.js library into a MEAN application using angular2. Here are the steps I've taken: npm install d3 tsd install d3 In mypage.ts file (where I intend to show the d3.js graph) // <reference path="../../../typings/d3/d3.d ...

Enhance TypeScript class declarations with additional properties

Our company has developed its own test framework for our software because we found it difficult to use an open-source framework within the context of our specific development needs. Recently, I took on the task of creating Typescript Definition Files to e ...

Add a class to a button in an Angular btn-group if a specific string is found within an

I am currently working on a project where I have multiple buttons that need to toggle an active class when selected in order to change their color. Below is a snippet of what I have: In the array called 'selected', I have: this.selected = [&ap ...

Removing an object from an array when a certain key value already exists in TypeScript

I'm currently facing an issue with my function that adds objects to an array. The problem arises when a key value already exists in the array - it still gets added again, but I want it to only add if it doesn't exist yet. Here's what I have: ...

Tips on overcoming errors while attempting to create a copy of an object using Spread, especially when the object's class contains abstract methods

In the code snippet below, there is an abstract class that requires extended classes to implement a specific method. However, when utilizing the "spread" syntax, an error occurs due to the missing implementation of the abstract method. abstract class Test ...

What is the best way to set up my page to detect the "enter" key input when the form is not created with the <form> tag?

Within the HTML code, data is received and stored in variables within my TypeScript file. At the end of the HTML, there is a button that was created separately from any user input containers. This button triggers a function that processes the information i ...

Failed to hit breakpoint in TypeScript file while working with Visual Studio 2019 and Webpack

We are working on an ASP.NET MVC 5 application where we are incorporating TypeScript for client-side code and using webpack to bundle everything into a single js file. TypeScript has been installed via npm. However, we have encountered an issue where setti ...

Converting SASS in real-time using SystemJS

I have been reading various blogs discussing the use of SystemJS and SASS transpiling, but most of the examples I come across involve pre-processing SASS files before importing them into JavaScript code. However, I am interested in being able to directly i ...

An issue arises when trying to group and sum an array of objects due to difficulty converting strings to arrays in TypeScript

Below is the provided code snippet: Definition of Interface - interface IWEXInterface { readonly Date?: string; "Exec Qty"?: string; readonly Expiry?: string; } Data Collection - let data: IWEXInterface[] = [ { Date: &qu ...

The function did not return a Promise or value as expected when using async and await

    I have been working on implementing this code structure for my cloud functions using httpRequest. It has worked seamlessly with those httpRequest functions in the past. However, I recently encountered an error when trying to use it with the OnWrite ...

Trying out Angular2 service using a fabricated backend

Just a heads up: I know Angular2 is still in alpha and undergoing frequent changes. I am currently working with Angular2 and facing an issue with testing an injectable service that has a dependency on http. I want to test this service using a mock backend ...

In the process of developing a custom Vue component library with the help of Rollup and VueJS 3

My goal is to develop a custom Vue component library using rollup and Vue.js. The process went smoothly with Vue2, but I encountered issues parsing CSS files with Vue3. To address this, I updated the dependencies in the package.json file. package.json { ...

Tips for creating a redirect to a specific page after clicking a link in an email using Angular

I've been working on implementing a feature in Angular where users can click on a link provided in an email and then get redirected to the respective page after authentication. I've tried a few different approaches, but none of them seem to be wo ...

Inserting a pause between a trio of separate phrases

I am dealing with three string variables that are stacked on top of each other without any spacing. Is there a way to add something similar to a tag in the ts file instead of the template? Alternatively, can I input multiple values into my angular compo ...