Surprising Outcome when Dealing with Instance Type - Allowing extra properties that are not explicitly stated in the type

I've come across an issue with the InstanceType Utility Type. I am trying to create a function that takes a class constructor and an instance of that class.

For instance:

export type Class<T=any> = { new(...args: any[]): T; };

function acceptClassAndInstance<T extends Class>(model: T, instance: InstanceType<T>) {
  
}

The problem is that the function accepts any object as long as it has the properties defined in the type, but it also allows objects with additional properties not specified in the type. Here's an example scenario:

export type Class<T=any> = { new(...args: any[]): T; };

class AModel {
  foo: string;

  constructor(params: Partial<Pick<AModel, keyof AModel>>) {
    Object.assign(this, params)
  }
}

class BModel {
  foo: string;
  bar: string;

  constructor(params: Partial<Pick<BModel, keyof BModel>>) {
    Object.assign(this, params)
  }
}

function acceptClassAndInstance<T extends Class>(model: T, instance: InstanceType<T>) {
  
}

/* This should be correct */
const instance = new AModel({ foo: "bar" })
acceptClassAndInstance(AModel, instance)

/* This should fail, but doesn't */
const instance2 = new BModel({ foo: "bar", bar: "baz"})
acceptClassAndInstance(AModel, instance2)

/* And this should fail too, but it doesn't */
const notAnInstance = { "foo": "bizz", "bar": "barrrrr", "anotherProperty": "should fail" }
acceptClassAndInstance(AModel, notAnInstance)

/* This fails as expected */
const notAnInstance2 = { "bar": "barrrrr", "anotherProperty": "should fail" }
acceptClassAndInstance(AModel, notAnInstance2)

Why is this happening? Is it a bug in TypeScript? Is there anything else I can do to achieve the desired outcome?

https://stackblitz.com/edit/vitejs-vite-bkqfyq?file=src%2Fmain.ts,tsconfig.json&terminal=dev

Answer №1

The reason for this behavior lies in the nature of typescript's structural type system, where excess properties are allowed. Essentially, if an object behaves like a duck (quacks like a duck), it is considered to be a duck.

To enforce that only instances created by a class constructor are accepted, you can introduce private fields within the class. By making these fields private, they are inaccessible outside of the class, preventing other types from implementing them.

class A {
    foo: string
}
const a: A = { foo: 'foo' } // valid


class B {
    foo: string
    private bar: string
}
const b: B = { foo: 'foo' } // error

In your specific case, adding a private field to AModel may help you achieve the desired type checking results.

class AModel {
  foo: string;

  private anything = 'A'

  constructor(params: Partial<Pick<AModel, keyof AModel>>) {
    Object.assign(this, params)
  }
}

Check Playground


However, keep in mind that resorting to such measures should only be done with caution and only if necessary. It is advisable not to intentionally circumvent default type checking unless absolutely needed.

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

ReactJs Error: Unable to access property value because it is undefined (trying to read '0')

I am currently attempting to retrieve and display the key-value pairs in payload from my JSON data. If the key exists in the array countTargetOptions, I want to show it in a component. However, I am encountering an error message stating Uncaught TypeError ...

Ways to verify if a certain type possesses an index signature during runtime

Is it possible to create a type guard in JavaScript that checks if a given type implements an index signature? I'm unsure if this concept would be viable, but here is the idea: I am looking for guidance on how to implement the logic within this funct ...

Remember to always call "done()" in Typescript + Mocha/Chai when dealing with async tests and hooks. Additionally, when returning a Promise, make sure it resolves correctly

It seems like I'm facing an old issue that I just can't seem to resolve, despite trying everything in my power. The error message reads: Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Pro ...

Generate a pre-signed URL for an AWS S3 bucket object using Typescript in NextJS, allowing for easy file downloads on the client-side using @aws-sdk/S3Client

In the utilization of v3 of the S3Client, it appears that all existing examples are based on the old aws-sdk package. The goal is for the client (browser) to access a file from S3 without revealing the key from the backend. From my research, it seems tha ...

Web application is not making API calls

Currently, I am experimenting with calling data from an API to create a simple weather application for practice purposes. I have been using the inspect element feature in my Chrome browser to check if the console will show the data log, but unfortunately, ...

Prevent resizing or zooming on AmCharts4 radar chart

Is there a way to disable the click-drag zooming feature on the AmCharts4 radar chart in my react application? Thank you. View image of the chart ...

Ensuring the accurate usage of key-value pairs in a returned object through type-checking

After generating a type definition for possible response bodies, I am looking to create a function that returns objects shaped as { code, body }, which are validated against the typing provided. My current solution looks like this: type Codes<Bodies> ...

The BehaviorSubject will consistently emit identical values to each subscription

I am currently facing an issue with the BehaviorSubject where it emits a value for every subscription. This means that if I have, for example, 2 subscriptions to this.projectService.projectState$ streams using methods like async or tap, the projectState$ e ...

Expect a reply within the loop

One of my endpoints takes some time to generate data, and I have another endpoint to retrieve that generated data. I make the initial call using await, extract the ID from the response, and then keep calling the second endpoint until the status is not "Suc ...

Issue encountered with ng-include compatibility in Angular 5

Just getting started with Angular and working on a small test project using Angular 5 and Visual Code. I'm attempting to use ng-include but the template is not displaying. src add-device add-device.component.html add-device.com ...

The form control is missing a specified name attribute, causing an error with the value accessor

<input type="email" class="form-control passname" [(ngModel)]="emailID" name="Passenger Email ID" placeholder="email" required pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$"/> <div class="shake-tool ...

The lite-server is not compatible for initiating the Angular2 Quickstart

Struggling to get the Angular2 Quick start app up and running on my Windows system. Unfortunately, I've hit a roadblock with the "lite-server". Despite installing dependencies (npm install), when attempting to run the app (npm start), an error pops u ...

Implementing a feature in ReactJS that allows users to upload multiple images in base64 format

I'm trying to develop an image uploader using base64 and I want the output as an array. However, I am encountering an issue where the array is coming out empty!. I suspect it might be due to an asynchronous problem. Any tips on how to incorporate asyn ...

Sometimes encounter undefined values after assigning them through the service

One of the challenges I am facing is handling public fields in my service: @Injectable() export class ShareDataService { // Some code public templateForCurrencyCOS; public templateForPercentCOS; public templateForCurrencyCOGS; public te ...

Having trouble resolving modules after generating tsconfig.json?

I recently added a tsx component to my next.js 13 project following the documentation. After creating the required tsconfig.json file, I encountered module not found errors when running npm run dev: $ npm run dev > [email protected] dev > n ...

Combine multiple objects to create a new object that represents the intersection of all properties

Imagine you have these three objects: const obj = { name: 'bob', }; const obj2 = { foo: 'bar', }; const obj3 = { fizz: 'buzz', }; A simple merge function has been written to combine these three objects into one: ...

Changing the color of a specific span using Angular

I am working with a dynamic mat-table where columns are added and populated on the fly. The table headers are styled using divs and spans. My goal is to change the color of a header to black when clicked, but also un-toggle any previously selected header. ...

Resolving the Angular NG0100 Error: Troubleshooting the ExpressionChangedAfterItHasBeenCheckedError

I am currently working on implementing a dialog component that takes in data through its constructor and then utilizes a role-based system to determine which parts of the component should be displayed. The component code snippet looks like this: export cl ...

I possess information stored within the array object below, and I aim to transform it into a different array object format

Here is the response I received from my API: let data = [ { date: '2021-04-27', formatted_date: 'Apr 27', location: [ { date: '2021-04-27', formatted_date: 'Apr 27', countr ...

Insert an HTML element or Angular component dynamically when a specific function is called in an Angular application

Currently, I am working on a component that contains a button connected to a function in the .ts file. My goal is to have this function add an HTML element or make it visible when the button is clicked. Specifically, I would like a dynamic <div> elem ...