Utilizing index signatures in TypeScript mapped types

Is there a way to abstract over the type

{ 'k': number, [s: string]: any }
by creating a type alias T such that T<'k', number> results in the desired type?


Take a look at the following example:

function f(x: { 'k': number, [s: string]: any }) {}                           // ok
type T_no_params = { 'k': number, [s: string]: any };                         // ok
type T_key_only<k extends string> = { [a in k]: number };                     // ok
type T_value_only<V> = { 'k': V, [s: string]: any};                           // ok
type T_key_and_index<k extends string, V> = { [a in k]: V, [s: string]: any };// ?
  • Directly using { 'k': number, [s: string]: any} as the type of the parameter in the function f is acceptable.
  • The indexed part [s: string]: any in a type alias works correctly
  • Using k extends string in a type alias also functions as expected
  • However, attempting to combine k extends string with [s: string]: any in the same type alias results in a parse error.

One approach that seems to work is:

type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V }

But it is unclear why this does not flag extra properties as errors (the type on the right side of the & should not allow objects with additional properties).


EDIT:

Despite the & being the intersection operator, behaving akin to set-theoretic intersection, it does not handle extra properties in the same way, as evidenced by the following example:

function f(x: {a: number}){};
function g(y: {b: number}){};
function h(z: {a: number} & {b: number}){};

f({a: 42, b: 58});  // does not compile. {a: 42, b: 58} is not of type {a: number}
g({a: 42, b: 58});  // does not compile. {a: 42, b: 58} is not of type {b: number}
h({a: 42, b: 58});  // compiles!

In the above example, it appears that {a: 42, b: 58} neither matches {a: number} nor {b: number}, yet it combines as {a: number} & {b: number}. This deviates from set-theoretical intersection principles.

This inconsistency raises doubts about how intersecting a mapped type with { [s: string]: any } could expand the type rather than restrict it.


I've come across similar inquiries like:

  • Index signature for a mapped type in Typescript
  • How do I add an index signature for a mapped type

Although they share a resemblance in name, they do not directly address the issue at hand.

Answer №1

type HasKeyAndValue<K extends string, V> = { [s: string]: any } & { [S in K]: V }
is the precise way to define the desired type. However, it's important to note that the keyof type operator can return string | number instead of just string when used on a type with a string index signature.

The keyof type operator will return string | number instead of string when applied to a type with a string index signature.

It's currently unknown how to restrict the index to only be the string type and not string | number. Allowing number to access string index seems reasonable as it aligns with Javascript behavior, where a number can always be stringified. Conversely, accessing a number index with a string value is not safe.


The & type operator functions like set theoretic intersection - it always limits the set of potential values (or keeps them the same, never expanding them). In this case, the type excludes any non-string-like keys as an index, specifically excluding unique symbols as an index.

Your confusion may arise from how Typescript handles function parameters. Passing parameters as explicitly defined variables differs from passing them as variables. In both scenarios, Typescript ensures that all parameters have the correct structure/shape; however, in the latter case, it also disallows additional properties.


View the code demonstrating these concepts:

type HasKeyAndValue<K extends string, V> = { [s: string]: any } & { [S in K]: V };
type WithNumber = HasKeyAndValue<"n", number>;
const x: WithNumber = {
  n: 1
};

type T = keyof typeof x; // string | number
x[0] = 2; // okay - number is treated as a string-like index
const s = Symbol("s");
x[s] = "2"; // error: cannot access via symbol

interface N {
  n: number;
}

function fn(p: N) {
  return p.n;
}

const p1 = {
  n: 1
};

const p2 = {
  n: 2,
  s: "2"
};

fn(p1); // okay - exact match
fn(p2); // okay - structural matching: { n: number } present; additional props ignored
fn({ n: 0, s: "s" }); // error: additional props not ignored when called explicitly
fn({}); // error: missing n property

EDIT

When creating object literals with a specific shape, like

const p: { a: number} = { a: 42 }
, Typescript treats them differently. Unlike regular structural inference, the type must be an exact match. This makes sense as those extra properties are inaccessible without additional potentially unsafe casting.

[...] However, TypeScript assumes that there might be a mistake in this code. Object literals are subject to excess property checking when assigned to different variables or passed as arguments. Any object literal properties not present in the "target type" will result in an error. [...] Assigning the object to another variable is a surprising workaround.

TS Handbook

Another method to address this error is to intersect it with { [prop: string]: any }.

Additional code examples:

function f(x: { a: number }) {}
function g(y: { b: number }) {}
function h(z: { a: number } & { b: number }) {}

f({ a: 42, b: 58 } as { a: number }); // compiles - casting possible, but `b` is inaccessible anyway
g({ a: 42 } as { b: number }); // does not compile - incorrect cast; Conversion of type '{ a: number; }' to type '{ b: number; }' may be a mistake
h({ a: 42, b: 58 }); // compiles!

const p = {
  a: 42,
  b: 58
};

f(p); // compiles - regular structural typing
g(p); // compiles - regular structural typing
h(p); // compiles - regular structural typing

const i: { a: number } = { a: 42, b: 58 }; // error: not an exact match
f(i); // compiles
g(i); // error
h(i); // error

Answer №2

Let's explore a different approach to understanding the intersection operator. Perhaps this analogy will provide some clarity:

type Combo = { x: string } & { y: number }

You can interpret Combo as "an entity that possesses a property x of type string and a property y of type number". This description also fits another straightforward type:

type Basic = { x: string; y: number }

Interestingly, these two types are interchangeable in most scenarios.

This should clarify why HasKeyValue essentially mirrors the type you were attempting to articulate.

Regarding the issue with T_key_and_index, it stems from the initial segment, [a in k]: V, which defines a mapped type. Mapped types do not allow additional properties. To include extra properties in a mapped type, you can utilize a type intersection using &.

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

esBuild failing to generate typescript declaration files while running in watch mode

Recently dove into using edBuild and I have to say, it's been a breeze to get up and running - simple, fast, and easy. When I execute my esBuild build command WITHOUT WATCH, I can see that the type files (.d.ts) are successfully generated. However, ...

What steps should I take to establish a one-to-one relationship with legacy tables?

As I work on developing a web application (angular, nestjs, typeorm), I am faced with the challenge of linking two legacy user tables together within our existing system. Despite my efforts, I continue to encounter an error message related to column refere ...

Tips for accessing the 'index' variable in *ngFor directive and making modifications (restriction on deleting only one item at a time from a list)

Here is the code snippet I'm working with: HTML: <ion-item-sliding *ngFor="let object of objectList; let idx = index"> <ion-item> <ion-input type="text" text-left [(ngModel)]="objectList[idx].name" placeholder="Nam ...

Transform a group of objects in Typescript into a new object with a modified structure

Struggling to figure out how to modify the return value of reduce without resorting to clunky type assertions. Take this snippet for example: const list: Array<Record<string, string | number>> = [ { resourceName: "a", usage: ...

Executing Promises in TypeScript Sequentially

I have a collection of doc objects and I need to send an API request for each doc id in the list, executing the requests in a sequential manner. In my Typescript code, I am using Promise.all and Promise.allSelected to achieve this. [ { "doc_id&q ...

Alerts appear immediately upon beginning to type, asking for 8 characters and ensuring both passwords match

Is it possible to notify users that both passwords should match and they need to enter at least 8 characters after typing? There is currently an issue where a notification appears for entering less than 8 characters, but the password reset still proceeds ...

Is there a solution for the error "Unable to persist the session" in a Next.js application that utilizes Supabase, Zustand, and Clerk.dev for authentication?

I have successfully set up a Next.js application with Clerk.dev for authentication and Supabase for data storage. I'm also leveraging Zustand for state management. However, an error is plaguing me, stating that there's "No storage option exists t ...

The method toLowerCase is not found on this data type in TypeScript

I am currently working on creating a filter for autocomplete material. Here is an example of my model: export class Country { country_id: number; name: string; } When calling the web method ws: this.ws.AllCountry().subscribe( ...

A guide on incorporating unique font weights into Material UI

Looking to customize the Material theme by incorporating my own font and adjusting the font weights/sizes for the Typography components. I am attempting to set 100/200/300/400/500/600/700 as options for each specific typography variant, but it seems that o ...

The attribute 'pixiOverlay' is not found in the property

Working on my Angular 8 project, I needed to display several markers on a map, so I chose to utilize Leaflet. Since there were potentially thousands of markers involved, I opted for Leaflet.PixiOverlay to ensure smooth performance. After installing and imp ...

Access-Control-Allow-Origin header not being sent by ExpressJS

In the midst of my project, I find myself needing an angular web application to connect with a node/express backend. Despite trying to implement Cors for this purpose, the express server refuses to send the Access-Control-Allow-Origin header. I am perplexe ...

Is there a method in typescript to guarantee that a function's return type covers all possibilities?

In the case of having a constant enum like: enum Color { RED, GREEN, BLUE, } A common approach is to create a helper function accompanied by a switch statement, as shown below: function assertNever(x: never): never { throw new Error(`Unexpecte ...

What is preventing me from setting the User object to null in my Angular application?

Currently, I am working on a project in Angular and encountering a specific issue. In my service class, the structure looks like this: export class AuthService { authchange: new Subject<boolean>(); private user: User; registerUser(authD ...

IE11 is throwing a fit because of a pesky long-running script error caused by the powerful combination of Webpack, React,

Utilizing webpack 4.* for bundling my react 16.* and typescript 3.* project has been causing issues on internet explorer 11. I consistently encounter a "not responding long running script error" on both local and test servers (in production mode). The lac ...

Having trouble with Angular 2's Output/emit() function not functioning properly

Struggling to understand why I am unable to send or receive some data. The toggleNavigation() function is triggering, but unsure if the .emit() method is actually functioning as intended. My end goal is to collapse and expand the navigation menu, but for ...

Eliminating every instance of the character `^` from a given string

I am encountering an issue with a particular string: "^My Name Is Robert.^". I am looking to remove the occurrences of ^ from this string. I attempted using the replace method as follows: replyText.replace(/^/g, ''); Unfortunately, thi ...

Is there a way for me to maintain a consistent layout across all pages while also changing the content component based on the URL route in Next.js?

I'm currently working with Typescript and Next.js My goal is to implement a unified <Layout> for all pages on my website. The layout comprises components such as <Header>, <Footer>, <Sidenav>, and <Content>. Here is the ...

Encountering a Typescript error while attempting to convert a JavaScript file to Typescript within an Express

After deciding to transition my express.js code to typescript, I have encountered an issue with my user model. Below is a simplified version of the code: //user.model.ts import { Schema, Types } from 'mongoose'; export interface User { na ...

Tips for converting a string array constant into a union type

I have a string array that I want to use to create a new type where the properties correspond to the elements in the array. There are different types of arrays and I have a function that generates different output types based on the input array. const RG ...

Angular 4 and Webpack: Compilation Error

After successfully running npm install, I encountered an error when trying to execute ng serve. Despite multiple attempts and troubleshooting, the issue persists. Could this be related to Angular versions? Interestingly, the same project runs smoothly on ...