Using Keyof on a type combined with Record<string, unknown> results in missing properties and can cause errors when paired with Omit<T, K>

I encountered a situation that has left me unsure whether it is an actual issue or simply a misunderstanding on my part.

Upon reviewing this code snippet:

type Props = {
  foo: string
  bar: string
} & Record<string, unknown>
// Using Record<string, unknown> seems to be the recommended way of saying "any object"

// Everything looks good, no errors: the known properties appear to be preserved in the intersection
const obj: Props = { foo: '', bar: '' }
type KeysOfProps = keyof Props // In this case, this type is "string"

These observations have led me to the following conclusions:

  • If keyof Props is string, then Props should not allow obj to have toto and tata as strings.
  • If { foo: string; bar: string } can be assigned to Props, then keyof Props should still include the known properties foo and bar. (Otherwise, foo and bar should be unknown in my understanding)

Despite all this, I have discovered that keyof Props is probably 'foo' | 'bar' | string, which simplifies to string.

However, this has led to some complications in my particular case. For instance:

// This results in Record<string, unknown>, causing 'bar' to be lost in the process
type PropsWithoutFoo = Omit<Props, 'foo'>

// This can be explained by how Omit is defined:
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Since `keyof T` is `string` and K is 'foo': Exclude<string, 'foo'> is string
// Pick<Props, string> becomes 'foo' | 'bar' | unknown which simplifies to unknown
// => All keys of the intersection are lost

This issue arose in a codebase involving generics, where Record<string, unknown> serves as a default type that can be substituted by any type that extends it. The loss of my properties appears to stem from this, hence the need to address this issue.

Answer №1

Everything is functioning correctly in TypeScript in this scenario. It seems that the bullet points you mentioned are actually misconceptions: when the keyof type operator is used with types containing string indexes, it becomes lossy. As you pointed out, 'foo' | 'bar' | string simplifies to just string; this is because a value of type Props can have any key that is a string. Therefore, using Pick<XXX, keyof XXX> on a type like XXX with a string index signature will make you lose the known keys:

type PropsMissingKnownKeys = Pick<Props, keyof Props>;
/* type PropsMissingKnownKeys = {
    [x: string]: unknown;
} */

It is important to note that this is not the same as using a direct mapped type in the form of K in keyof XXX; in that scenario, the compiler will treat in keyof differently, iterating over each known key as well as the indexer:

type HomomorphicProps = { [K in keyof Props]: Props[K] };
/* type HomomorphicProps = {
    [x: string]: unknown;
    foo: string;
    bar: string;
} */

This leads to a potential alternative implementation of Omit that uses the support for key remapping in mapped types introduced in TypeScript 4.1:

type AlternativeOmit<T, K extends PropertyKey> = {
    [P in keyof T as Exclude<P, K>]: T[P]
}

AlternativeOmit<T, K> is a direct mapped type that iterates over each property key P in T, remapping each key to never or P based on whether or not P is assignable to K. By using this instead of Omit, you get the desired type:

type PropsWithoutFoo = AlternativeOmit<Props, "foo">;
/* type PropsWithoutFoo = {
    [x: string]: unknown;
    bar: string;
} */

Since AlternativeOmit and Omit exhibit different behaviors, there might be cases where Omit is preferred over AlternativeOmit. It is not recommended to replace all instances of Omit with AlternativeOmit, and changing the definition of Omit in TypeScript's library would impact other users. As mentioned in a relevant GitHub issue comment (microsoft/TypeScript#31501):

All possible definitions of Omit have certain trade-offs; we've chosen one we think is the best general fit and can't really change it at this point without inducing a large set of hard-to-pinpoint breaks.

Playground link to code

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

Patience is key when waiting for one observable to respond before triggering another in Angular's async environment

What is my goal? I have several components with similar checks and data manipulation tasks. I am looking to centralize these tasks within an observable. To achieve this, I created an observable named "getData" in my service... The complexity lies in the f ...

Exploring Parquet Files with Node.js

Looking for a solution to read parquet files using NodeJS. Anyone have any suggestions? I attempted to use node-parquet but found it difficult to install and it struggled with reading numerical data types. I also explored parquetjs, however, it can only ...

Creating a React prop type validation that is dependent on the value of another prop

I am in the process of creating a custom React Table component, with the following TableProps structure: export interface ColumnType<ItemType, Key extends keyof ItemType = keyof ItemType> { header: string; key?: keyof ItemType; renderCell: (val ...

The absence of a semicolon following the interface declaration is the issue

I am facing a slight issue with ESLint and Typescript, particularly regarding semicolons after declaring interfaces. Additionally, I utilize VSCode as my editor with automatic formatting upon saving. Below is the configuration in my .eslintrc.json file: ...

Nest may struggle with resolving dependencies at times, but rest assured they are indeed present

I've encountered a strange issue. Nest is flagging a missing dependency in a service, but only when that service is Injected by multiple other services. cleaning.module.ts @Module({ imports: [ //Just a few repos ], providers: [ ServicesService, ...

Is there a way to convert a typescript alias path to the Jest 'moduleNameMapper' when the alias is for a specific file?

I am currently working on setting up Jest in a TypeScript project. In our tsconfig.json file, we are using path aliases like so: "baseUrl": ".", "paths": { "@eddystone-shared/*": [ "../shared/*" ], "@eddystone-firebase-helpers/*": [ "src/helpers/fire ...

What's the trick to aligning the label on the material-ui text field to the right side?

I am working on a React project with an RTL layout using material-ui and typescript. I am struggling to align the label of a text field to the right. Can anyone help me with this? https://i.sstatic.net/UrkIF.jpg ...

The axios GET request failed to return a defined value

My current issue involves making a get request using the following code snippet: router.get('/marketUpdates',((request, response) => { console.log("market updates"); var data: Order[] axios.get('http://localhost:8082/marketUpdates& ...

Organized modules within an NPM package

I am looking to develop an NPM package that organizes imports into modules for better organization. Currently, when I integrate my NPM package into other projects, the import statement looks like this: import { myFunction1, myFunction2 } from 'my-pac ...

Each property of an object has its own unique key, yet they all share the same data type

I have a single-use object with only three properties, all of which should be of the same type. The code below currently achieves this, but I'm curious if there is a more efficient way to declare the type for timingsObject: let timingsObject: ...

The program encountered an error with code TS2339, indicating that the property "name" cannot be found on the type "never"

app.component.html I'm attempting to display the response data from my Firebase project using *ngFor. <div class="row"> <div class="col-md-3"> <h4 class="text-warning">All Employee Da ...

What is the best way to interpret a line break within a string variable in TypeScript?

Realtime Data base contains data with \n to indicate a new paragraph. However, when this data is retrieved and stored in a String variable, the website fails to interpret the \n as a paragraph break: https://i.stack.imgur.com/tKcjf.png This is ...

What is the best way to ensure that the operations are not completed until they finish their work using RX

Is there a way to make RXJS wait until it finishes its work? Here is the function I am using: getLastOrderBeta() { return this.db.list(`Ring/${localStorage.getItem('localstorage')}`, { query: { equalTo: fa ...

When onSubmit is triggered, FormData is accessible. But when trying to pass it to the server action, it sometimes ends up as null

I am currently utilizing NextJS version 14 along with Supabase. Within my codebase, I have a reusable component that I frequently utilize: import { useState } from 'react'; interface MyInputProps { label: string; name: string; value: stri ...

"Upon invoking the services provider in Ionic 2, an unexpected undefined value was

I encountered an issue while setting a value in the constructor of my page's constructor class. I made a call to the provider to fetch data. Within the service call, I was able to retrieve the data successfully. However, when I tried to access my vari ...

Local font not applying styles in Tailwind CSS

I integrated the Gilroy font into my application, but I am facing issues with tailwindcss not being able to style the text properly. The font appears too thin in all elements such as paragraphs and headers. Here is the file structure for reference: https: ...

Creating a versatile function that can function with or without promises is a valuable skill to have

I am currently working on developing a versatile sort function that can function with or without promises seamlessly. The intended structure of the function should look something like this: function sort<T>(list: T[], fn: (item: T) => string | nu ...

Access a designated webpage with precision by utilizing Routes in Angular

Is it possible to display a different component in Angular routing based on a condition in the Routing file? For example, if mineType is equal to "mino", can I navigate to another component instead of the one declared in the Routing? Should I use Child ro ...

The ngModel directive is present here

In my Angular project, I've observed the use of two different syntaxes for binding ngModel: [(ngModel)]="this.properties.offerValue" and [(ngModel)]="properties.offerValue". Although they appear to function identically, it has sparked my curiosity ab ...

Two unnamed objects cannot be combined using the AsyncPipe

Currently, I am looking to implement an autocomplete feature using Angular Material in Angular 8. Below is a snippet of the code used in the TypeScript file: @Input() admins: User[]; userGroupOptions: Observable<User[]>; filterFormFG: FormGrou ...