How can one specify a type in Typescript with a precise number of properties with unspecified names?

Imagine I have a variable with a name and a value, both of which I need for a specific task such as logging. This can be achieved in the following way:

const some_variable = "abcdef"
const another_variable = 12345

const log1 = (name: string, value: any) => console.log(name, value)

log1("some_variable", some_variable)
log1("another_variable", another_variable)
log1("some_variable", some_variable, "another_variable", another_variable) // results in a compile time error
log1() // results in a compile time error

A more efficient approach would be

log2({some_variable})
log2({another_variable})

To achieve this, I can create a function

const log2 = (obj: { [key: string]: any }) => {
    const keys = Object.keys(obj)
    if( keys.length !== 1 ) throw new Error("only one key is expected")
    const key = keys[0]
    console.log(key, obj[key])
}

However,

log2({some_variable, another_variable}) // compiles but results in a run time error
log2({}) // compiles but results in a run time error

I want to force compilation errors on lines like log2({v1, v2}) and log2({}).
I aim to eliminate dynamic type checks and ensure that there is a compile time check to validate that the obj parameter has only one key, or an exact number of keys with names that are unknown to me.

Answer №1

If you do some intricate manipulations with the type system, it's possible to make the compiler raise an error if your object contains more than one known key. This snippet illustrates a potential approach:

type OneKey<T, K extends keyof T = keyof T> =
    string extends K ? never : number extends K ? never :
    K extends any ? { [P in keyof T]?: P extends K ? T[P] : never } : never;    

function log2<T extends object & OneKey<T>>(obj: T): void;
function log2(obj: any) {
    const keys = Object.keys(obj)
    if (keys.length !== 1) throw new Error("only one key is expected")
    const key = keys[0]
    console.log(key, obj[key])
}

This restricts T to object types without index signatures because objects with index signatures cannot be guaranteed to have only one key. It verifies that the object can be assigned to a "one-key" version of itself. For example, for an object like {a: string, b: number}, it checks against

{a?: string, b?: undefined} | {a?: undefined, b?: number}
. If the check fails as in this case, it indicates that the object has multiple keys. However, if you pass an object of type {a: string}, the checked type is {a?: string}, which matches the criteria.

To test its functionality, consider the following examples:

log2({ some_symbol }); // okay
log2({ some_symbol, some_other_symbol }); // error
//   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '{ some_symbol: string; some_other_symbol: number; }' is not assignable 
// to type '{ some_symbol?: undefined; some_other_symbol?: number | undefined; }'.
log2({}); // error
//   ~~
// Type '{}' is not assignable to parameter of type 'never'.

Seems promising on initial inspection!

Note that there may be various edge cases where this setup might exhibit unexpected behavior due to limitations in the type-checking system. Objects with optional keys or those involving unions could lead to peculiar outcomes. Proceed cautiously!

Hoping this sheds light on your query. Best wishes!

Access the Playground link here

Answer №2

const log2 = (data: { [name: string]: any }) => {
    const names = Object.keys(data)
    // in the examples you provided, there are cases with 2 keys and 0 keys,
    // which causes an error as per your code logic
    if( names.length !== 1 ) throw new Error("only one key is expected")
    const name = names[0]
    console.log(name, data[name])
}

I'm not sure what result you anticipated. The code compiles correctly but may give a runtime error depending on input.

You might have intended to use the function like this:

log2({symbol_one: another_symbol})

This way, it sets up a dictionary with only one key, meeting your code's requirements.

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

Sometimes, Express may return the message "not found" on and off

After working with express for many years, I find myself a bit out of practice with TypeScript - and it seems like my eyesight is failing me! This is the first time I've encountered this issue, so I must be missing something... My current dilemma is ...

The Angular2 project using ag-grid-enterprise is currently experiencing difficulties with implementing the License Key

I have a valid license for the ag-grid-enterprise version, but I'm struggling with how to integrate it into my Angular2 project. I've attempted placing the license in the main.ts file using LicenseManager and specifying the enterprise version in ...

Is jest the ideal tool for testing an Angular Library?

I am currently testing an Angular 9 library using Jest. I have added the necessary dependencies for Jest and Typescript in my local library's package.json as shown below: "devDependencies": { "@types/jest": "^25.1.3", "jest": "^25.1.0", ...

What is the method for launching Chrome synchronously in Selenium WebDriver using createSession()?

After executing the code below using Selenium WebDriver to launch a Chrome browser: import { Driver } from 'selenium-webdriver/chrome'; Driver.createSession(); console.log("I've launched!"); I'm encountering an issue where "I've ...

Unfortunately, the utilization of an import statement outside a module is restricted when working with Electron

Is there a solution to the well-known problem of encountering the error message "Cannot use import statement outside a module" when working with an Electron-React-Typescript application? //const { app, BrowserWindow } = require('electron'); impor ...

Issue with Nuxt: Property accessed during rendering without being defined on the instance

As I attempt to create cards for my blog posts, I encountered an issue with a Post component in my code. The cards are displaying like shown in the picture, but without any text. How do I insert text into these cards? Currently, all the text is within attr ...

What is the best way to utilize the Moment.js TypeScript definition file in a website that already has moment.min.js integrated?

Currently, I am in the process of transitioning a website to utilize TypeScript by converting one JavaScript file at a time. All pages on my site are already linked to moment.js, such as: <script src="/scripts/moment.min.js"></script> I have ...

The concept of passing arguments in Typescript

During my experience with Typescript programming, I encountered a situation like the one described below. If I pass an argument containing an object with the same name as the parameter defined in the function signature, Typescript recognizes it, but not ...

The React Component in Next.js does not come equipped with CSS Module functionality

I have been attempting to incorporate and apply CSS styles from a module to a React component, but the styles are not being applied, and the CSS is not being included at all. Here is the current code snippet: ./components/Toolbar.tsx import styles from & ...

Choosing the Right Language for AngularJS 2: TypeScript, JavaScript, or Dart?

AngularJS 2 is on the horizon, and the documentation recommends three languages: Typescript, Javascript, and Dart. As someone who primarily works with Javascript EcmaScript 5, I'm curious about the strengths and weaknesses of these three options. Cu ...

Dependency management with various versions of an NPM package

I'm feeling a bit puzzled about NPM package versions. In my ionic2 app's packages.json file, I have a dependency on [email protected]. Additionally, I have the latest version of ionic-native which is dependent on [email protected]. Th ...

Anticipated the object to be a type of ScalarObservable, yet it turned out to be an

When working on my Angular project, I utilized Observables in a specific manner: getItem(id): Observable<Object> { return this.myApi.myMethod(...); // returns an Observable } Later, during unit testing, I successfully tested it like so: it(&apos ...

Testing React Components - The `useClient` function should only be used within the `WagmiConfig` component

In my Next.js app with TypeScript, Jest, and React testing library, I encountered an error while trying to test a component. The error message states that `useClient` must be used within `WagmiConfig`. This issue arises because the `useAccount` hook relies ...

Generate user-customized UI components from uploaded templates in real-time

Summary: Seeking a solution to dynamically generate UI pages using user-provided templates that can be utilized for both front-end and back-end development across various use cases. Ensuring the summary is at the top, I am uncertain if this question has b ...

The type 'typeof globalThis' does not have an index signature, therefore the element is implicitly of type 'any'. Error code: ts(7017) in TypeScript

I'm encountering an issue with my input handleChange function. Specifically, I am receiving the following error message: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.ts(7017) when att ...

Avoid accessing members in Vue 3 using TypeScript that may be unsafe

Recently, we initiated the process of upgrading from Quasar v1 to Quasar v2 (moving from Vue 2 to Vue 3). In the past, this code functioned without any issues: // src/pages/myComponent.vue <script lang="ts"> import { defineComponent } from ...

Is that possible to prevent the use of the & symbol in Angular 4 HTTP requests?

Using an HTTP request to save data in my database. The code snippet I used is: const form = '&inputdata=' + myinput + '&rf_date=' + rf_date; return this.http.post(this.rootUrl, form, {headers : this.reqHeader}); In thi ...

Obtaining the dimensions of each individual child component within an NgTemplate

I have the following code snippet within my template. While I can iterate through its components using `get`, it does not return an object that allows me to access deeper into the HTML attributes. <ng-template #container></ng-template> Compon ...

What is the best way to inform TypeScript that the output of the subscribe method should be recognized as an array containing elements of type

I'm facing a challenge understanding types while working with noImplicitAny and typescript in Angular 6. The compiler is indicating that the type of result is Object, even though I am certain it should be an array of type Manufacturer. Unable to assig ...

Implementing advanced error handling using custom error messages with enums

I'm trying to use Zod to validate a gender field with z.nativeEnum(), but for some reason my custom error messages are not being applied: gender: z.nativeEnum(Gender, { invalid_type_error: 'Le sexe doit être homme ou femme.', ...