Why am I unable to apply the keyof operator from one type to another type, even though both types have identical keys defined but different value types?

Consider this code snippet. I am encountering a TypeScript error specifically on the last compat[k] line with the following error message:

Type 'keyof T' cannot be used to index type 'Partial<CompatType>'

export type KeysOfType<T, U, B = false> = {
  [P in keyof T]: B extends true
    ? T[P] extends U
      ? U extends T[P]
        ? P
        : never
      : never
    : T[P] extends U
    ? P
    : never;
}[keyof T];

export type BigIntKeys<T> = KeysOfType<T, bigint, true>

export type CompatType<T> = Omit<T, BigIntKeys<T>> &
  {
    [Property in BigIntKeys<T>]: string;
  };

export function compatModel<T>(model: T): CompatType<T> {
  const compat: Partial<CompatType<T>> = {};
  for (const k of Object.keys(model) as Array<keyof T>) {
    const v = model[k];
    compat[k] = typeof v === "bigint" ? v.toString() : v;
  }
  return compat as CompatType<T>;
};

The keys of both types should align completely, but there is a discrepancy in the type of values within the objects. This inconsistency should allow me to use the keys of one type to access the other, however, it does not seem to work that way. Am I missing something or making a mistake somewhere?

TS Playground

Answer №1

One of the limitations of TypeScript lies in its design; refer to microsoft/TypeScript#28884. As mentioned in this comment, "complementary subsets of a higher order type, constructed using Pick<T, K> or by other means, is not assignable back to that higher order type."

For instance, a type like

Omit<T, K> & Record<K, string>
where K extends keyof T will not be recognized as having the same keys as T, even though logically it should. This discrepancy occurs because the compiler fails to equate
Exclude<keyof T, K> | Extract<keyof T, K>
with keyof T when T and/or K are unspecified generic types:

function foo<T, K extends keyof T>(a: keyof T) {
  const b: Extract<keyof T, K> | Exclude<keyof T, K> = a; // error!
}

In situations where the exact types for T and K are unknown, the compiler defers the evaluation of

Extract<keyof T, K> | Exclude<keyof T, K>
, hence failing to recognize their equivalence to keyof T.


To address this issue, you can construct CompatType as a homomorphic mapped type directly from T, employing a conditional type to determine whether the specific key K from keyof T should belong to BigIntKeys<T> and selecting the value type accordingly:

type CompatType<T> = { [K in keyof T]: 
  T[K] extends bigint ? bigint extends T[K] ? string : T[K] : T[K] 
}

This approach results in more visually appealing types,

type Check = CompatType<{ a: string, b: bigint, c: number, d: boolean }>;
/* type Check = {
    a: string;
    b: string;
    c: number;
    d: boolean;
} */

Furthermore, the compiler acknowledges that CompatType<T> indeed shares the same keys as T, even in scenarios involving generic types for T:

export function compatModel<T>(model: T): CompatType<T> {
  const compat: Partial<CompatType<T>> = {};
  for (const k of Object.keys(model) as Array<keyof T>) {
    const v = model[k];

    compat[k]; // no error here

    compat[k] = typeof v === "bigint" ? v.toString() : v; // still error here, unrelated
  }
  return compat as CompatType<T>;
};

Nevertheless, an error may surface while attempting to assign

typeof v === "bigint" ? v.toString() : v
to compat[k] due to the compiler's inability to validate if something is assignable to a conditional type.


In situations where you are confident in the correctness of your code despite the compiler's doubts, you have the option of utilizing type assertions or resorting to the any type to relax type checking enough to appease the compiler:

export function compatModel<T>(model: T): CompatType<T> {
  const compat: Partial<Record<keyof T, any>> = {};
  for (const k of Object.keys(model) as Array<keyof T>) {
    const v = model[k];
    compat[k] = typeof v === "bigint" ? v.toString() : v;
  }
  return compat as CompatType<T>;
};

Here, we inform the compiler to ignore concerns regarding the property value types of compat and ultimately return it as CompatType<T>. As long as you are absolutely certain about the accuracy of the typings, this practice is acceptable. However, exercising caution is advised:

const hmm = compatModel({ a: Math.random() < 10 ? 3n : 3 });
hmm.a // number | bigint
if (typeof hmm.a !== "number") {
  3n * hmm.a; // no compile time error, but runtime issue: "cannot convert BigInt to number" 
}

The a property is inferred as number | bigint, resulting in the compiler assuming that hmm.a might hold a bigint, which is impossible. While there exist remedies to such dilemmas, these fall beyond the scope of the current discussion length.

Please be mindful when resorting to type assertions or any to mitigate compiler errors, as they increase the likelihood of potentially critical errors slipping through unnoticed.

Link to Playground with Code Example

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

Guide on importing absolute paths in a @nrwl/nx monorepo

I am currently working on a @nrwl/nx monorepo and I am looking to import folders within the project src using absolute paths. I attempted to specify the baseUrl but had no success. The only solution that did work was adding the path to the monorepo root ts ...

When trying to update a form field, the result may be an

Below is the code for my component: this.participantForm = this.fb.group({ occupation: [null], consent : new FormGroup({ consentBy: new FormControl(''), consentDate: new FormControl(new Date()) }) }) This is th ...

What is the step-by-step guide for implementing an access limiter on an interface

When attempting to modify the access level on an interface in Typescript, I encountered an error stating 'cannot appear in type member.' After removing the access limiter on the interface and implementing it, I tried changing the access limiter o ...

Integration of Mocha with WebStorm

WebStorm offers a useful feature that adds a small arrow next to describe() and it() keywords when writing tests with Mocha, allowing for easy manual execution. However, there is a challenge: I require additional setup before each test, leading me to use ...

Problem with Grouping of Columns in Material-UI

Having some trouble with column grouping in MUI data grid pro. I am using typescript and trying to implement column grouping, but encountering issues with the module GridColumnGroupingModel, which is used as the type definition for columnGroupingModel. Fol ...

Header Express does not contain any cookies, which may vary based on the specific path

In my express.js app, I have two controllers set up for handling requests: /auth and /posts. I've implemented token authorization to set the Authorization cookie, but I'm encountering an issue when making a request to /posts. The request goes th ...

Tips for fetching individual item information from Firebase real-time database using Angular 9 and displaying it in an HTML format

product.component.ts import { AngularFireDatabase } from '@angular/fire/database'; import { ProductService } from './../product.service'; import { ActivatedRoute } from '@angular/router'; import { Component, OnInit} from &apo ...

NextRouter does not have a property called "refresh"

Here is the provided code snippet: "use client"; import { useRouter } from "next/router"; import { useState } from "react"; export default function CreatePrompt() { const [title, setTitle] = useState(""); const ...

I am experiencing difficulties with TypeORM connecting to my Postgres database

Currently, I am utilizing Express, Postgres, and TypeORM for a small-scale website development project. However, I am encountering challenges when it comes to establishing a connection between TypeORM and my Postgres database. index.ts ( async ()=>{ ...

Arrange a JavaScript map based on its values and ensure that a specific field index remains at the top position

I'm sure this question may seem simple to some, but as a JavaScript novice, I couldn't find the answer myself. Here is the code snippet I'm working with: Let map = new Map<String,String> map.set('0', select) map.set('1&a ...

Using TypeScript with AWS Lambda: To package imports or not to package? Alternatively: Error in Runtime.ImportModule: Module '@aws-sdk/...' not found

I have been working with the code in my lambda.ts file, attempting to execute it on an AWS Lambda: import 'aws-sdk' import { /* bunch of stuff... */ } from "@aws-sdk/client-cloudwatch-logs"; import {Context, APIGatewayProxyResult} from ...

The information in the map cannot be inferred by Typescript based on the generic key type

I encountered a TypeScript issue while refactoring an existing feature in my app. It seems that TypeScript is having trouble picking up the correct type when passing the generic type through nested functions. enum App { AppOne = 'app-one', A ...

Determine the specific data types of the component properties in React Storybook using TypeScript

Currently, I am putting together a component in the storybook and this is how it appears: import React, { useCallback } from 'react'; import { ButtonProps } from './types'; const Button = (props: ButtonProps) => { // Extract the nec ...

Avoid Inferring as a Union Type

I am currently working on implementing a compact type-safe coordinate management system in TypeScript. It revolves around defining the origin of the coordinate as a type parameter, with functions that only accept one specific origin type. Below is a short ...

Issues encountered when attempting to add a new user on Firebase

I am facing an issue with this function that is supposed to add new users to my firebase database, but for some reason, it's not working. exports.createUserWithEmailAndPassword = functions.https.onCall( async(data, context) => { const { ...

Launching a Node.js command-line interface to NPM, developed using TypeScript

I'm struggling with deploying my Node CLI tool to NPM. During development and testing, everything works fine. I can even use `npm link` on the repo without any issues. After successfully publishing and downloading the package, the application crashes ...

What is the approach to forming a Promise in TypeScript by employing a union type?

Thank you in advance for your help. I am new to TypeScript and I have encountered an issue with a piece of code. I am attempting to wrap a union type into a Promise and return it, but I am unsure how to do it correctly. export interface Bar { foo: number ...

What is the best way to utilize project references with multiple tsconfig files?

Let's say I have three separate projects to work on: shared frontend backend In order to use the shared project as a reference in both the frontend and the backend, I need to make a few adjustments. The backend utilizes commonjs modules while the fr ...

Ionic: Automatically empty input field upon page rendering

I have an input field on my HTML page below: <ion-input type="text" (input)="getid($event.target.value)" autofocus="true" id="get_ticket_id"></ion-input> I would like this input field to be cleared eve ...

Exploring Typescript: Uncovering the Secrets of the navigator.connection Property

I am trying to access the NetworkInformation interface by using a simple TypeScript function like the one shown below: private checkNetworkConnection(): void { const connection = Navigator.connection || navigator.mozConnection || navigator.webkitConn ...