In TypeScript, specifying that a type should only extend from an object does not effectively prevent strings from being accepted

My goal is to ensure proper typing for an attributes object that is stored in a wrapper class. This is necessary to maintain correct typing when accessing or setting individual attributes using the getOne/setOne methods as shown below.

However, I am facing an issue where strings are being accepted as parameters for the AttributeCollection constructor, even though this goes against the intended purpose of the class:

type AttributeMap<M extends object> = {
  [key in keyof M]: M[key];
};

export class AttributeCollection<M extends object> {
  constructor(private attributes: AttributeMap<M>) {}

  public getOne<K extends keyof AttributeMap<M>>(key: K): M[K] {
    return this.attributes[key];
  }

  public setOne<K extends keyof AttributeMap<M>>(key: K, value: M[K]): void {
    this.attributes[key] = value;
  }
}

const acCorrectImplicit = new AttributeCollection({ a: 'str'}); // ok
const acCorrectExplicit = new AttributeCollection<{ a: string }>({ a: 'str'}); // ok

const acWrong = new AttributeCollection('str'); // ok, but shouldn't be

/*

The obtained typing:

const acWrong: AttributeCollection<{
    toString: () => string;
    charAt: (pos: number) => string;
    charCodeAt: (index: number) => number;
    concat: (...strings: string[]) => string;
    indexOf: (searchString: string, position?: number | undefined) => number;
    ... 37 more ...;
    [Symbol.iterator]: () => IterableIterator<...>;
}>

*/

const am: AttributeMap<'str'> = 'str'; // error

console.log(acCorrectImplicit, acCorrectExplicit, acWrong, am);

I suspect there might be some incorrect type passing occurring somewhere - can you assist me in identifying what's causing this issue?

Check out the playground.

Answer №1

Upon inspection, it seems that the mapped type known as AttributeMap<M> results in widening the inferred type argument from a primitive string to the String interface (an object wrapper type) without being rejected. This behavior might be considered either a bug or a feature of TypeScript, and as of now no existing issue has been easily identified.

If you want to ensure compatibility with the object type when generating output from AttributeMap, you can achieve this by intersecting with object:

type AttributeMap<M extends object> = object & {
  [K in keyof M]: M[K];
};

By implementing the above solution, everything functions as intended:

const acCorrectImplicit = new AttributeCollection({ a: 'str' }); // works fine
const acCorrectExplicit = new AttributeCollection<{ a: string }>({ a: 'str' }); // works fine
const acWrong = new AttributeCollection("str"); // throws an error

In the given scenario, using AttributeMap serves no apparent purpose. It essentially acts as an identity function for types, hence making AttributeMap<M> almost equivalent to M. By directly utilizing M, the problem is resolved:

export class AttributeCollection<M extends object> {
  constructor(private attributes: M) { }

  public getAttributes(): M {
    return this.attributes;
  }
}

const acCorrectImplicit = new AttributeCollection({ a: 'str' }); // works fine
const acCorrectExplicit = new AttributeCollection<{ a: string }>({ a: 'str' }); // works fine
const acWrong = new AttributeCollection("str"); // throws an error

Access Playground link to view code

Answer №2

Resolution:

To prevent TypeScript from inferring primitive methods as objects, utilize NoInfer<M>:

export class AttributeCollection<M extends object> {
  constructor(private attributes: AttributeMap<NoInfer<M>>) {}

  public getAttributes(): AttributeMap<NoInfer<M>> {
    return this.attributes;
  }
}

Please note that this functionality requires typescript@^5.4.

const acWrong = new AttributeCollection("string"); // error will be thrown as expected

Interactive Playground

This issue seems to be a bug in TypeScript where primitive methods are incorrectly inferred as objects rather than their actual type.

Answer №3

AttributeMap seems unnecessary. Perhaps a more straightforward type would be more fitting here.

export class AttributeCollection<M extends Record<string, unknown>> {
  constructor(private attributes: M) {}

  public getOne<K extends keyof M>(key: K): M[K] {
    return this.attributes[key];
  }

  public setOne<K extends keyof M>(key: K, value: M[K]): void {
    this.attributes[key] = value;
  }
}

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

Transforming JSON in Node.js based on JSON key

I am having trouble transforming the JSON result below into a filtered format. const result = [ { id: 'e7a51e2a-384c-41ea-960c-bcd00c797629', type: 'Interstitial (320x480)', country: 'ABC', enabled: true, ...

Error: Parsing error - Unrecognized character "@" in Angular module

Currently I am delving into the realm of webpack and attempting to integrate it into my project. However, I seem to have hit a roadblock as I encounter the following error. Despite my efforts to troubleshoot and research, I cannot seem to find a loader spe ...

Searching and adding new elements to a sorted array of objects using binary insertion algorithm

I'm currently working on implementing a method to insert an object into a sorted array using binary search to determine the correct index for the new object. You can view the code on codesanbox The array I have is sorted using the following comparis ...

Discover the best method for retrieving or accessing data from an array using Angular

In my data processing task, I have two sets of information. The first set serves as the header data, providing the names of the columns to be displayed. The second set is the actual data source itself. My challenge lies in selecting only the data from th ...

Avoiding the use of reserved keywords as variable names in a model

I have been searching everywhere and can't find a solution to my unique issue. I am hoping someone can help me out as it would save me a lot of time and effort. The problem is that I need to use the variable name "new" in my Typescript class construct ...

How to update an Angular 2 component using a shared service

My question is regarding updating components in Angular 4. The layout of my page is as follows: Product Component Product Filter Component Product List Component I am looking to link the Product Filter and Product List components so that when a user c ...

Error: Unable to set value, val.set is not a defined function for this operation (Javascript

Encountering a problem while running the function val.set(key, value), resulting in a type error TypeError: val.set is not a function within the file vendor-es2015.js. Here's the simplified code snippet: import { Storage } from '@ionic/storage& ...

Removing redundant names from an array using Typescript

My task involves retrieving a list of names from an API, but there are many duplicates that need to be filtered out. However, when I attempt to execute the removeDuplicateNames function, it simply returns an empty array. const axios = require('axios&a ...

Generating dynamic content

I require assistance with a programming issue. I am working with two separate components: Stage and Executor. Within the Stage component, I am attempting to create new elements based on input parameters, while in the Executor component, I set these paramet ...

What is the best way to sort through this complex array of nested objects in Typescript/Angular?

tableData consists of an array containing PDO objects. Each PDO object may have zero or more advocacy (pdo_advocacies), and each advocacy can contain zero or more programs (pdo_programs). For example: // Array of PDO object [ { id: 1, ...

Execute service operations simultaneously and set the results in the sequence they are received

I am faced with a challenge involving multiple service methods that fetch data from various servers. The responses from these APIs come in at different times, and I need to store the responses in variables as soon as they are received. Here are my service ...

What is the way to declare a prop as optional in Svelte using TypeScript?

I am facing an issue in declaring an optional prop in Svelte with TypeScript. The error message I receive is "Declaration or statement expected". Can someone guide me on how to correctly declare the prop? Definition of My Type: export enum MyVariants { ...

Determining the Type<> of a component based on a string in Angular 2

Can you retrieve the type of a component (Type<T>) based on a string value? For example: let typeStr: string = 'MyComponent'; let type: any = getTypeFromName(typeStr); // actual type ...

Transferring Cookies through FETCH API using a GET method from the client-side to the server-side

Struggling with a challenge here: Attempting to send a cookie via a GET request to determine if the user is logged in. The cookie is successfully transmitted to my browser and is visible in the developer tools. When I manually make a request through the UR ...

Typescript is issuing warnings when displaying errors for the RTK query

I am currently using React Ts and Rtk query. My goal is to display the API response error on the UI. While it's working, I am encountering a warning that prompts me to set an error type for the response errors. How can I incorporate an error type for ...

Exploring ways to conduct a thorough scan of object values, inclusive of nested arrays

My goal is to extract all values from an object. This object also includes arrays, and those arrays contain objects that in turn can have arrays. function iterate(obj) { Object.keys(obj).forEach(key => { console.log(`key: ${key}, value: ${o ...

Leveraging TypeScript 2.1 and above with extended tsconfig configurations

Recently, I have been experimenting with the new extends feature in the tsconfig.json file that allows developers to create a base configuration which other modules can extend or modify. Although it is functional, it is not working as anticipated. Strange ...

Using Two Unique Typeface Options in Material UI for ReactJS

Currently, in my React App, I am utilizing the Material UI library with Typescript instead of regular Javascript. I've encountered a small hurdle that I can't seem to overcome. The two typefaces I want to incorporate into my app are: Circular-S ...

The type definition file for '@types' is not present in Ionic's code base

After updating my Ionic 6 project to use Angular 3, everything works perfectly in debug mode. However, when I attempt to compile for production using 'ionic build --prod' or 'ionic cordova build android --prod', I encounter the followin ...

Can dynamic string types be declared in Typescript?

Let's consider the following scenario: export enum EEnv { devint, qa1 }; export type TEnv = keyof typeof EEnv; export const env:Record<TEnv, {something:number}> = { devint: { something: 1, }, qa1: { something: 1, }, } Now, I ai ...