What is preventing the TypeScript program below from generating a type error?

Review the program below.

interface Eq<A> {
  eq(this: A, that: A): boolean;
};

class Pair<A> implements Eq<Pair<A>> {
  constructor(public x: A, public y: A) {}

  eq(this: Pair<A>, that: Pair<A>): boolean {
    return this.x === that.x && this.y === that.y;
  }
}

class Triple<A> implements Eq<Triple<A>> {
  constructor(public x: A, public y: A, public z: A) {}

  eq(this: Triple<A>, that: Triple<A>): boolean {
    return this.x === that.x && this.y === that.y && this.z === that.z;
  }
}

const eq = <A extends Eq<A>>(x: A, y: A): boolean => x.eq(y);

console.log(eq(new Pair(1, 2), new Triple(1, 2, 3)));
console.log(eq(new Triple(1, 2, 3), new Pair(1, 2)));

I find it surprising that there are no type errors thrown by the TypeScript compiler in the last two lines of code. In theory, applying the eq function to different types should result in an error. The actual output from the program is a mix of true and false.

What might be causing the TypeScript compiler to overlook these type errors in the given program? Is there a way to configure it to detect and flag such discrepancies?

Answer №1

The code successfully compiles thanks to the utilization of structural subtyping in TypeScript, a concept that differs from nominal subtyping found in many other programming languages.

It's important to note that the Triple class can be assigned to a variable of type Pair:

const p: Pair<number> = new Triple(1, 2, 3);

In the provided example:

console.log(eq(new Pair(1, 2), new Triple(1, 2, 3)));
console.log(eq(new Triple(1, 2, 3), new Pair(1, 2)));

The type of eq is automatically inferred as:

const eq: <Pair<number>>(x: Pair<number>, y: Pair<number>) => boolean

This demonstrates that Triple is an acceptable argument for a parameter of type Pair, ensuring smooth compilation.

To mimic nominal subtyping, you could introduce a different private field in your classes. In this specific instance, you have two potential options:

  • Include an additional marker field
  • Privatize x, y, and z, then provide getters

For more insights on enforcing nominal typing in TypeScript, refer to Can I force the TypeScript compiler to use nominal typing?

Answer №2

Initially, my vote goes to appreciating @VLAZ for the insightful comments.

It seems that both Pair and Triple are implementing Eq, which could explain why there isn't a compilation error.

In addition, according to @VLAZ's comment, it appears that TS does not provide support for higher kinded types.

Given the usage of F-bounded polymorphism, it might be beneficial to offer TS a hint regarding the second argument.

interface Eq<A> {
  eq(this: A, that: A): boolean;
};

class Pair<A> implements Eq<Pair<A>> {
  constructor(public x: A, public y: A) { }

  eq(this: Pair<A>, that: Pair<A>): boolean {
    return this.x === that.x && this.y === that.y;
  }
}

class Triple<A> implements Eq<Triple<A>> {
  constructor(public x: A, public y: A, public z: A) { }

  eq(this: Triple<A>, that: Triple<A>): boolean {
    return this.x === that.x && this.y === that.y && this.z === that.z;
  }
}

// hint is here :D
const eq = <Fst extends Eq<Fst>, Scd extends Eq<Scd> & Fst>(x: Fst, y: Scd): boolean => x.eq(y);

const x = eq(new Pair(1, 2), new Triple(1, 2, 3)); // ok
const y = eq(new Triple(1, 2, 3), new Pair(1, 2)); // error

This may not be a comprehensive solution but I hope it provides some useful pointers.

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

Dynamically change or reassign object types in TypeScript

Check out the example you can copy and test in TypeScript Playground: class GreetMessage { message: {}; constructor(msg: string) { let newTyping: { textMsg: string }; // reassign necessary this.message = newTyping; this.message.textMsg = msg; ...

Can I include `ChangeDetectorRef` in an Angular 4 service constructor?

In my service file, I have a system that alerts about Subjects. The value is set and then reset to null after 3 seconds using the setTimeout() method. However, upon adding changeDetection: ChangeDetectionStrategy.OnPush to the app.component.ts, it appears ...

send mouse event to component when clicked

Looking to retrieve mouse coordinates in an Angular component. This is the HTML snippet: <div id="container" class="fullSize" style="height:100%;" (click)="onClick(ev)"></div> And here's the corresponding function in the TypeScript file ...

Enforce numerical input in input field by implementing a custom validator in Angular 2

After extensive research, I was unable to find a satisfactory solution to my query. Despite browsing through various Stack Overflow questions, none of them had an accepted answer. The desired functionality for the custom validator is to restrict input to ...

Angular is failing to detect a change in the initial element of an array

In my Angular app, I am working on displaying a list of dates for the current week. Users should be able to view previous weeks by clicking a button, so I am using an Observable to update the array of dates and trying to display the updated array. Althoug ...

Tips for minimizing a collection of objects?

Currently, I am working on developing a unique quiz format where there are no correct or incorrect answers; instead, responses are given a score ranging from 1 to 4. Each question falls under one of four distinct categories (such as cat, dog, rabbit, alpa ...

Comparing Static Factory Methods and Constructors

This question led to an interesting discussion in the comments, focusing on the advantages of Static Factory Methods versus Constructors. Consider the following example: export class User extends Model<UserProps> { // Recommended Method (Without ...

Why is it that in reactive forms of Angular, the parameter being passed in formControlName is passed as a string?

I am currently working on a reactive form in Angular. In order to synchronize the FormControl object from the TypeScript file with the form control in the HTML file, you need to utilize the formControlName directive. This is accomplished as shown below: f ...

What is the reason for TypeScript's prioritization of arguments over assignment in generic inference?

When it comes to Typescript generics inference, the priority is given to arguments over assignment. This means that any param props are automatically converted into type unknown, even if they are assigned to a variable whose type param is set to an object ...

Updating Angular 8 Component and invoking ngOninit

Within my main component, I have 2 nested components. Each of these components contain forms with input fields and span elements. Users can edit the form by clicking on an edit button, or cancel the editing process using a cancel button. However, I need to ...

A guide on effectively utilizing BehaviorSubject for removing items from an array

Currently, I am working on an Angular 8 application that consists of two components with a child-parent relationship. It came to my notice that even after removing an item from the child component, the item remains visible in the parent component's li ...

Gain insights on Stripe Webhooks with Firebase Functions and Node.js

I've been struggling to integrate Firebase functions with Stripe webhooks for listening to events. Take a look at my code: exports.stripeEvents = functions.https.onRequest((request, response) => { try { const stripeSignature = request. ...

quickest method for retrieving a property by name, regardless of its location within the object hierarchy

I am looking for a solution to retrieve the value of a property from an object graph, and I am wondering if there is a more efficient alternative to the method I have outlined below. Instead of using recursion, I have chosen tree traversal because it cons ...

Unlocking the power of asynchronous methods within the useEffect() hook

When attempting to fetch an API within a useEffect(), I encountered the following error: Error: Invalid hook call. Hooks can only be called inside of the body of a function component. Here is the code snippet that caused the error: -API being fetched: ex ...

Clicking on an element- how can I find the nearest element?

Encountering an issue with my next js app where I am attempting to assign a class to an element upon clicking a button. The problem arises when trying to access the next div using the following code snippet: console.log(e.target.closest('.request_quot ...

Typescript error in React: The element is implicitly of type any because a string expression cannot be used to index type {}

I'm currently working on grouping an array by 'x' in my React project using TypeScript, and I've encountered the following error message: Element implicitly has an 'any' type because expression of type 'string' can&a ...

How can we make sure that a specific type member is present in an object in TypeScript?

My current dilemma involves working with an object that will hold data. Specifically, I am dealing with a type called UserPrompt, as well as nodes and edges objects stemming from the Reactflow library. The challenge I'm facing is ensuring that the Us ...

I keep encountering the issue where nothing seems to be accessible

I encountered an error while working on a project using React and Typescript. The error message reads: "export 'useTableProps' (reexported as 'useTableProps') was not found in './useTable' (possible exports: useTable)". It ...

What is the solution for correcting a glitch in smooth scrolling behavior that occurs following the page's initial rendering?

Upon loading the page or when the user updates it, there seems to be an issue with smooth scrolling to the desired position on the site (lower than needed). How can this be resolved so that everything functions correctly even after the page has finished r ...

The Javascript IIFE prohibits accessing a variable before it has been initialized

I encountered an error message saying "Uncaught ReferenceError: Cannot access 'Q12' before initialization" while working in the changeCssClassIfOldAndNewValuesAreDifferent function. Any thoughts on what might be causing this issue? Thank you. ...