Unexpected lack of error in Typescript intersection type and function signature

I have defined the following data structures:

type SampleA = {
    a: string;
}

type SampleB = {
    b: number;
}

type SampleC = {
    c: boolean;
}

type Samples = SampleA &
    SampleB &
    SampleC;

Next, I utilize the defined types in the following manner:

function customize(pattern: { [key: string]: string }) {
    console.log(pattern);
}

const data: Samples = { a: 'example', b: 123, c: true }; 
customize(data);

Surprisingly, the typescript compiler does not generate any errors when invoking the customize(data) function, despite the data:Samples variable not aligning with the function's signature of customize.

Playground link

Why is typescript not displaying any errors? Is this a potential bug within the compiler?

Answer №1

The main reason behind the success of this method lies in the capability of an intersection type to be assigned to its base types.

When it comes to an intersection type, Examples can easily be assigned to ExampleA. Furthermore, ExampleA can be assigned to { [key: string]: string }. Consequently, Examples is certainly assignable to the function's parameter type.

The above explanation is exemplified in the following code snippet:

const bar: Examples = { a: 'foo', b: 1, c: false }; 
const bar2: ExampleA = bar;
const bar3: { [key: string]: string } = bar2;
foo(bar3); //It works
foo(bar2); //Since bar3 = bar2 works, this must work as well
foo(bar); //Since bar2 = bar works, this must work too

Playground version


UPDATE

The concept becomes significant when you want to follow the principle that states "when A is assignable to B and B is assignable to C, then A must be assignable to C". The type system is designed to allow such assignments. However, there is a potential issue when passing the value as a parameter to foo.

You can assign a value to a variable of a type that shares only a subset of the members of the assigned value. For instance, this assignment is valid:

let item: { a: string, b: number } = { a: "Hello World!", b: 1 };
let partiallyMatchingItem: { a: string } = item;

Having more properties in partiallyMatchingItem than declared in the type is completely acceptable. The guarantee is minimal.

However, the assignment to a mapped type fails because of the additional member of type number in item:

let item = { a: "Hello World!", b: 1 };
let mappedTypeItem: { [key: string]: string } = item; //Error

This time, the guarantee is not minimal but absolute. It can be easily bypassed intentionally or accidentally:

let item = { a: "Hello World!", b: 1 };
let partiallyMatchingItem: { a: string } = item;
let mappedTypeItem: { [key: string]: string } = partiallyMatchingItem;

Or simply:

let item = { a: "Hello World!", b: 1 };
let mappedTypeItem: { [key: string]: string } = item as { a: string };

This potential error is looming, especially when iterating through the properties of

mappedTypeItem</code assuming all values are of type <code>string
.

Considering the prevalent use of structurally typed assignments in TypeScript, this absolute guarantee contradicts the system of minimum guarantees provided by the type system.

A feasible solution would be to prevent values of "regular" types from being assigned to mapped types (backwards compatibility can be toggled with a switch in the tsconfig.json file). It is advisable to avoid such assignments as the type safety offered here is weak.

Answer №2

If you want to intentionally trigger the error, you can define Example as an interface instead of an intersection type. This has been possible since version 2.2, where interfaces can extend object types or even intersection types.

type ExampleA = {
    a: string;
}

type ExampleB = {
    b: number;
}

type ExampleC = {
    c: boolean;
}

interface Examples extends ExampleA, ExampleB, ExampleC {

}

function foo(pattern: { [key: string]: string }) {
    console.log(pattern);
}

const bar: Examples = { a: 'foo', b: 1, c: false }; 

foo(bar); // will result in an error

To further clarify the distinction between interface and intersection types, you can do it in the following manner:

type Examples = ExampleA &
    ExampleB &
    ExampleC;

interface IExamples extends Examples { // an empty interface "collapses" the intersection 
}

const bar1: Examples = { a: 'foo', b: 1, c: false };  
foo(bar1); // no error

const bar2: IExamples = { a: 'foo', b: 1, c: false };  
foo(bar2); // error

Another approach, as suggested by Titian in the comments, is to use a mapped type to construct an object type from the intersection, which closely resembles its generic parameter:

type Id<T> = { [P in keyof T]: T[P] } 

const bar3: Id<ExampleA & ExampleB & ExampleC> = { a: 'foo', b: 1, c: false };

foo(bar3); // will result in an error

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

Focusing on an input element in Angular2+

How do I set focus on an input element? Not with AngularDart, but similar to the approach shown in this question: <input type="text" [(ngModel)]="title" [focus] /> //or <input type="text" [(ngModel)]="title" autofocus /> Does Angular2 provi ...

Encountering syntax errors with CommonJS Rollup plugin when attempting to import third-party libraries, particularly those involving the 'process' module

I have been developing a personalized rollup configuration that involves React projects and inlines the JS and CSS in index.html. When attempting to import third-party React libraries (such as material-ui-color), I encountered an issue with CommonJS repo ...

Creating a new function within the moment.js namespace in Typescript

I am attempting to enhance the functionality of the moment.js library by adding a new function that requires a moment() call within its body. Unfortunately, I am struggling to achieve this. Using the latest version of Typescript and moment.js, I have sear ...

Create a series of actions that do not depend on using only one occurrence of the WriteBatch class

My goal is to create a series of batch actions using functions that do not require a specific instance of WriteBatch. Currently, I am passing an instance of the WriteBatch class to the functions so they can utilize the .set(), .update(), or .delete() metho ...

The left-hand operator in Typescript is not valid

I am a beginner in typescript and I have been following a tutorial called Tour of Heroes on Angular's website. In the final chapter of the tutorial, when I tried to use HTTP with the provided code, everything seemed to run fine but I encountered an er ...

An unexpected runtime error occurred: TypeError - Unable to use map function on events

When fetching data using graphQL and rendering it on the page, an error occurs: Unhandled Runtime Error TypeError: events.map is not a function I'm unsure if my useState declaration is correct. const [events, setEvents] = useState < any > ([]) ...

Unpacking Objects in JavaScript and TypeScript: The Power of Destructuring

I have a variable called props. The type includes VariantTheme, VariantSize, VariantGradient, and React.DOMAttributes<HTMLOrSVGElement> Now I need to create another variable, let's name it htmlProps. I want to transfer the values from props to ...

Is it possible for Angular 7 to disconnect from the internet?

I am looking to disable all buttons, clicks, and hyperlinks while displaying a backdrop with the message "GO ONLINE". It may come off as rude, but it is necessary. AppComponent (TS): The connectionMonitor can be used to monitor network connectivity. pr ...

"Mastering the art of debouncing in Angular using

I am facing an issue where, during a slow internet connection, users can press the save button multiple times resulting in saving multiple sets of data. This problem doesn't occur when working locally, but it does happen on our staging environment. E ...

Instructions for designing a Loading Indicator or Progress Bar within the App Directory of NextJS

Having built a substantial web application using NextJS 13, I initially utilized the Pages Router. However, as I neared completion of the website, I decided to make the switch to the App Directory. The primary motivation behind this migration was not just ...

Struggling to make the paste event function in Typescript

Here is a snippet of my Typescript code: const pasteFn = (e: ClipboardEvent) => { const { clipboardData } = e; const text = clipboardData?.getData('text/plain'); console.log(text); }; window.addEventListener('paste', pas ...

Angular does not display HTML content until the requested http data has been fully loaded

I am experiencing an issue where certain HTML content does not load until the component has received data from an API call through a service. Below is the relevant code snippet: import { ApiService } from './services/api.service'; @Component({ ...

The absence of the @Injectable annotation is causing an issue when importing a JSON

I am currently in the process of integrating a JSON file into my service by using a @inject() tag within the constructor. Below is the code snippet I am working with. DependencyInjectionContainer.ts import { Container, decorate, injectable } from "invers ...

Using a generic name as a JSON key in a TypeScript file

I received data from an API call const tradingApiResponse = { Timestamp: "2024-01-15T12:00:00", Ack: "Success", Version: 1, Build: "1.0.0", UserDeliveryPreferenceArray: { NotificationEnable: [ { EventType: ...

What is the reason that I am able to form an object using string literals, but introducing generics complicates the process?

My goal is to create an Object using string literals. export type MyCustomType<T extends string> = { propertyFromCustomType: T; }; export type CustomTypeWithLiteral<T extends string> = { [P in `${T}_with_literal`]: number; }; When my cre ...

React typescript wormhole is struggling to display content due to createPortal and useContext combination

Explore the interactive demo here. Purpose: The main objective is to have all children elements of PortalEntrance rendered beneath PortalExit. Main concept: PortalProvider provides a ref PortalExit assigns the ref to one of its child elements PortalEntr ...

Error: The method that is annotated with @action in MobX is not defined

I'm encountering a peculiar problem with the MobX annotations, where a method marked with @action seems to be missing from the resulting object. Let's consider the following TypeScript code snippet as a basic example: export class Car { @ob ...

Unlock the power of TypeScript by linking together function calls

I am looking for a way to create a type that allows me to chain functions together, but delay their execution until after the initial argument is provided. The desired functionality would be something like this: const getStringFromNumber = pipe() .then ...

What is the best way to decouple api and async request logic from React components while incorporating Recoil?

Currently, I find myself inserting my request/api logic directly into my components because I often need to set state based on the response from the backend. On my settings page, I have a function that saves the settings to recoil after the user clicks sa ...

Troubleshooting React TypeScript in Visual Studio Code

I've recently set up an ASP Core project with the React TypeScript template, but I'm encountering difficulties when it comes to debugging. The transition between the TypeScript code and the corresponding generated JavaScript code is proving to be ...