Verify additional keys in the output of a function

Imagine this scenario:

type Keys = 'x' | 'y' | 'z'
type Items = { [K in Keys]?: number }
let items: Items = { x: 1, y: 4 }

The result is:

Type '{ x: number; y: number; }' cannot be assigned to type 'Items'.
  Object literal can only specify known properties, and 'y' does not exist in type 'Items'

Therefore, it does not permit additional keys on an object.

However, consider the following:

type Function = () => Items
const foo: Function = () => ({ x: 1, y: 4 })

TypeScript accepts it, even though the function clearly returns a non-Items type.

Furthermore, using

const foo: Function = () => ({ x: true, y: 4 })
results in

Type 'boolean' is not assignable to type 'number | undefined'.(2322)
input.tsx(63, 12): The expected type is derived from property 'x' which is declared in type 'Items'

In essence, it verifies the returned value but seems indifferent to surplus keys.

Demo

Why does this occur, and is there a way to prohibit extra keys in the returned value in this situation?

Answer №1

Take note that the object {a: 1, d: 4} falls into the category of the Rec type. In TypeScript, object types typically permit additional properties and do not adhere strictly to being "exact" as specified in microsoft/TypeScript#12936. This behavior is reasoned by factors relating to subtyping and assignability. For instance:

class Foo {a: string = "";}
class Bar extends Foo {b: number = 123;}
console.log(new Bar() instanceof Foo); // true

It's important to acknowledge that every Bar includes a Foo, hence it is not possible to assert that "all Foo objects solely possess an a property" without impeding class or interface inheritance and extension. Moreover, since interface functions similarly, and given that TypeScript's typing system is inherently structural rather than nominal, there is no need to explicitly declare a Bar type for its existence:

interface Foo2 {a: string};
// interface Bar2 extends Foo2 {b: number};
const bar2 = {a: "", b: 123 };
const foo2: Foo2 = bar2; // valid

Hence, whether positive or negative, we are confined within a type system where surplus properties do not interfere with type compatibility.


Naturally, this feature can lead to errors. Therefore, when assigning a brand new object literal to a specific object type, there exist comprehensive property checks that act as though the type were exact. These checks only activate under certain circumstances, such as in your initial example:

let rec: Rec = { a: 1, d: 4 }; // notification about excess property

However, return values from functions presently do not fall under these conditions. The type of the return value broadens before any superfluous property checks take place. An age-old unresolved GitHub issue, microsoft/TypeScript#241, suggests altering this behavior so that return values from functions refrain from expanding in this manner, although an attempt at rectification was initiated at microsoft/TypeScript#40311, ultimately discontinued, possibly never integrating into the language.


No perfect methods exist to repress surplus properties universally. My recommendation is to accept that objects might contain additional keys and validate that any code you compose remains intact regardless of this scenario. You can implement measures that dissuade surplus properties, like these:

// expressly specify return type
const fn2: Func = (): Rec => ({ a: 1, d: 4 }) // notification of excess property

// apply a generic type sensitive to extra properties
const asFunc = <T extends Rec & Record<Exclude<keyof T, keyof Rec>, never>>(
    cb: () => T
): Func => cb;
const fn3 = asFunc(() => ({ a: 1, d: 4 })); // mistake! number is not supposed to be 'never'

Nevertheless, these approaches are intricate and prone to breaking, as nothing prevents you entirely from executing this action despite your efforts to safeguard your Func type:

const someBadFunc = () => ({ a: 1, d: 4 });
const cannotPreventThis: Rec = someBadFunc();

Conceiving code that anticipates additional properties usually involves maintaining an array of recognized keys. Consequently, abstain from doing this:

function extraKeysBad(rec: Rec) {
    for (const k in rec) { 
        const v = rec[k as keyof Rec];  
        console.log(k + ": " + v?.toFixed(2))
    }
}

const extraKeys = {a: 1, b: 2, d: "four"};
extraKeysBad(extraKeys); // a: 1.00, b: 2.00, RUNTIME ERROR! v.toFixed not a function

Instead, opt for:

function extraKeysOkay(rec: Rec) {
    for (const k of ["a", "b", "c"] as const) {
        const v = rec[k];
        console.log(k + ": " + v?.toFixed(2))
    }
}

extraKeysOkay(extraKeys); // a: 1.00, b: 2.00, c: undefined

Access the Playground code link

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

The type x cannot be assigned to the parameter '{ x: any; }'

Currently learning Angular and Typescript but encountering an error. It seems to be related to specifying the type, but I'm unsure of the exact issue. Still new at this, so any guidance is appreciated! src/app/shopping-list-new/shopping-edit/shopp ...

TS: How can we determine the type of the returned object based on the argument property?

Assume we have the following data types type ALL = 'AA' | 'BB' | 'CC'; type AA = { a: number; }; type BB = { b: string; }; type CC = { c: boolean; }; type MyArg = { type: ALL }; I attempted to create a mapping between type n ...

Bundle Angular library exports along with its corresponding models

I am in the process of developing an angular library for our company's private npm repository. Within this library, I aim to export classes that are utilized (injected via @Input()) in the library components. Here is a sample model: export class AdsT ...

Passing a class as a parameter in Typescript functions

When working with Angular 2 testing utilities, I usually follow this process: fixture = TestBed.createComponent(EditableValueComponent); The EditableValueComponent is just a standard component class that I use. I am curious about the inner workings: st ...

Do Angular lifecycle hooks get triggered for each individual component within a nested component hierarchy?

I'm exploring the ins and outs of Angular lifecycle hooks with a conceptual question. Consider the following nested component structure: <parent-component> <first-child> <first-grandchild> </first-grandchild& ...

A guide on leveraging typeof within function parameters to ensure accurate variances

Let's create a simple class hierarchy and a list of instances. The goal is to filter items from the list of instances based on their class. There are a couple of challenges: We cannot use the typeof T syntax. How can this be written? We cannot decla ...

The PathLocationStrategy function is designed to work exclusively within a local

I am facing a hash problem in my current project. Interestingly, everything works correctly in the test project. I have searched on Google for solutions but couldn't find any satisfactory answers: Angular2 without hash in the url When I add {provide ...

Having trouble fixing TypeScript bugs in Visual Studio Code

I am encountering a similar issue as discussed in this solution: Unable to debug Typescript in VSCode Regrettably, the suggested solution does not seem to resolve my problem. Any assistance would be greatly appreciated. My directory structure looks like ...

Error message: 'DialogContent' component is dependent on 'DialogTitle' component and needs to be implemented in NextJs and ReactJs with ShadCN

I've encountered the same error multiple times in my project, even after double-checking that each DialogContent component contains a DialogTitle within it. After thoroughly inspecting all my imports to ensure they are from ShadCN and not mistakenly ...

Prioritize the timepicker over the use of a modal window

Having an issue with my time picker in Angular being blocked by a modal window. Component.ts open() { const amazingTimePicker = this.atp.open(); amazingTimePicker.afterClose().subscribe(time => { console.log(time); }); } // T ...

How can you resolve the error message "No overload matches this call." while implementing passport.serializeUser()?

Currently, I am working on implementing passport.serializeUser using TypeScript. passport.serializeUser((user: User, done) => { done(null, user.id) }); The issue I am encountering is as follows: No overload matches this call. Overload 1 of 2, &ap ...

``Infinite loops in rxjs: how to create a never-ending task

I'm working on a never-ending task that needs to loop continuously in TypeScript. The next task should only start when the previous one has finished. I've decided to utilize rxjs for this because it appears to be the most concise approach. My ...

Tips for resolving the error message "termsOfUse must be a boolean type, but the final value was: 'on'," using a combination of Ionic, React, Yup, and Resolvers

I need to develop a registration form with all fields mandatory using React Hook Form and Yup for validation. The issue I'm facing is related to two checkboxes that are required. Upon form submission, I encounter the following error message for the ch ...

Enroll a nearby variable "Data" to an Observable belonging to a different Component within an Angular application

Looking to update the HTML view using *ngIf, depending on a local variable that should change based on an observable variable from a shared service. HTML <div class="login-container" *ngIf="!isAuthenticated"> TypeScript code for the same componen ...

Regular Expressions: Strategies for ensuring a secure password that meets specific criteria

Struggling to craft a regex for Angular Validators pattern on a password field with specific criteria: Minimum of 2 uppercase letters Minimum of 2 digits At least 1 special character. Currently able to validate each requirement individually (1 uppercase ...

Is there a way to initiate a mouse click and drag action in amCharts v5?

I am currently utilizing the capabilities of amCharts v5 to create a similar functionality to this particular example from amCharts v3. In the sample scenario, an event is triggered by the property "chart.isMouseDown" and alters the position of bullets ba ...

Include a control within a form based on the Observable response

I am looking to enhance my form by adding a control of array type, but I need to wait for the return of an Observable before mapping the values and integrating them into the form. The issue with the current code is that it inserts an empty array control e ...

Leveraging external Javascript files in Ionic framework version 3 and above

Hey there, I'm currently working on integrating the Mapwize SDK, an external Javascript library, with the latest version of Ionic. I've encountered the common challenge of getting Javascript to function smoothly with Typescript. Although I'm ...

Monitor constantly to determine if an element is within the visible portion of the screen

For a thorough understanding of my query, I feel the need to delve deeper. While I am well-versed in solving this issue with vanilla Javascript that is compatible with typescript, my struggle lies in figuring out how to invoke this function throughout th ...

Annotating Vue Computed Properties with TypeScript: A Step-by-Step Guide

My vue code looks like this: const chosenWallet = computed({ get() { return return wallet.value ? wallet.value!.name : null; }, set(newVal: WalletName) {} } An error is being thrown with the following message: TS2769: No overload ...