Uncovering the Secrets of Typescript Mixins: Leveraging Shared Properties Across Combined Classes

I am currently exploring the concept of mixins as explained in the TypeScript documentation.

https://www.typescriptlang.org/docs/handbook/mixins.html

You can find a playground setup here.

My question revolves around defining functions like jump() and duck() to manipulate properties x and y within the Sprite class.

In essence, I would like these methods to affect the x and y properties directly in the Sprite class. Is there a way to achieve this without adding x and y props to jumpable and duckable separately?

I'm finding it difficult to see practical use cases for TS mixins without the ability to seamlessly interact with properties across mixed-in classes.

The code snippet is provided below:

(() => {
  function applyMixins(derivedCtor: any, constructors: any[]) {
    constructors.forEach((baseCtor) => {
      Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
        Object.defineProperty(
          derivedCtor.prototype,
          name,
          Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
            Object.create(null)
        );
      });
    });
  }

  class Jumpable {
    jump() {
      console.log("jump");
    }
  }

  class Duckable {
    duck() {
      console.log("duck");
    }
  }

  class Sprite {
    x = 0;
    y = 0;
  }

  interface Sprite extends Jumpable, Duckable {}
  
  applyMixins(Sprite, [Jumpable, Duckable]);

  let player = new Sprite();
  player.jump();
  player.duck();
  console.log(player.x, player.y);
})();

Answer №1

When you utilize declaration merging, you are essentially informing the compiler about the functionality of your mixins since it cannot deduce that on its own. The documentation refers to this practice as an "alternative pattern", emphasizing that "this pattern relies less on the compiler, and more on your codebase to ensure both runtime and type-system are correctly kept in sync."

Therefore, be prepared for some additional complexity in persuading the compiler that your seemingly independent classes like Jumpable and Duckable can access properties such as x and y which are not explicitly declared within them. One straightforward approach is defining an interface representing the desired structure:

interface Positioned {
  x: number,
  y: number
}

You can then specify that the jump() and duck() methods should function on objects adhering to that structure by utilizing a this parameter:

class Jumpable {
  jump(this: Positioned) {
    console.log("jump");
    this.y += 2;
  }
}

class Duckable {
  duck(this: Positioned) {
    console.log("duck");
    this.y -= 1;
  }
}

This setup allows for modifying x and y inside the method while also warning against using the mixin methods incorrectly as regular methods:

const j = new Jumpable();
j.jump(); // compiler error
// The 'this' context of type 'Jumpable' is not 
// assignable to method's 'this' of type 'Positioned'.

After implementing these changes, you'll observe seamless functionality:

class Sprite {
  name = "";
  x = 0;
  y = 0;
  constructor(name: string) {
    this.name = name;
  }
}
interface Sprite extends Jumpable, Duckable { }
applyMixins(Sprite, [Jumpable, Duckable]);

const player = new Sprite("Player");
console.log(player.name, player.x, player.y); // Player 0 0
player.jump(); // jump
console.log(player.name, player.x, player.y); // Player 0 2
player.duck(); // duck
console.log(player.name, player.x, player.y); // Player 0 1

Hence, if you opt for the alternative pattern, you will have a smooth experience.


The recommended non-alternative mixin approach involves utilizing class factory functions that leverage standard class inheritance to extend a base class with the mixin functionalities. By restricting the base class to constructors of Positioned objects, the mixins gain access to the base class's x and y properties:

function Jumpable<TBase extends new (...args: any[]) => Positioned>(Base: TBase) {
  return class Jumpable extends Base {
    jump() {
      console.log("jump");
      this.y += 2;
    }
  };
}

function Duckable<TBase extends new (...args: any[]) => Positioned>(Base: TBase) {
  return class Duckable extends Base {
    duck() {
      console.log("duck");
      this.y -= 1;
    }
  };
}

The class expressions within the Jumpable and Duckable factory functions enable jump() and duck() to interact with this.y, given that the Base constructor aligns with type TBase, known for generating a subtype of Positioned.

For applying mixin methods to prototypes effortlessly, simply invoke the mixin factory functions on constructors:

class BaseSprite {
  name = "";
  x = 0;
  y = 0;
  constructor(name: string) {
    this.name = name;
  }
}
const Sprite = Jumpable(Duckable(BaseSprite));

No declaration merging is required here; the compiler automatically grasps that instances of Sprite exhibit traits of Positioned alongside possessing the methods jump() and duck():

const player = new Sprite("Player");
console.log(player.name, player.x, player.y); // Player 0 0
player.jump(); // jump
console.log(player.name, player.x, player.y); // Player 0 2
player.duck(); // duck
console.log(player.name, player.x, player.y); // Player 0 1

Link to Play 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

AsExpression Removes Undefined from Type More Swiftly Than I Prefer

Utilizing an API that returns a base type, I employ the as keyword to convert the type into a union consisting of two sub-types of the original base type. interface base { a: number; } interface sub1 extends base { s1: number; } interface sub2 extends bas ...

What is the most effective way to use a withLatestFrom within an effect when integrating a selector with props (MemoizedSelectorWithProps) sourced from the action?

I am struggling to utilize a selector with props (of type MemoizedSelectorWithProps) in an effect inside WithLatestFrom. The issue arises because the parameter for the selector (the props) is derived from the action payload, making it difficult for withLat ...

An error is triggered when an HttpClient post does not return any data

While sending a post request from my angular application to a web api, I am encountering an issue. The response from the api is supposed to be either a 200 status or a 404 status without any data being returned. An example of some headers for the 200 respo ...

Discovering items located within other items

I am currently attempting to search through an object array in order to find any objects that contain the specific object I am seeking. Once found, I want to assign it to a variable. The interface being used is for an array of countries, each containing i ...

Unable to invoke a function in TypeScript from a Kendo template within the Kendo TreeList component

In my TypeScript file for class A, I am encountering an issue with the Kendo TreeList code. I am trying to call a function from the Kendo template. export class A{ drillDownDataSource: any; constructor() { this.GetStatutoryIncomeGrid ...

Dynamic import of a SASS file in VueJS using a variable such as process.env

Is there a way to dynamically import a file using environment variables? I want to include a specific client's general theme SCSS to my app.vue (or main.ts) I'm thinking of something along the lines of: <style lang="sass"> @import"./th ...

How to start Angular2 prototype with an object literal

export abstract class GridColumn { public field?: string; public sortField?: string; public header?: string; public footer?: string; public sortable?: any = true; public editable?: boolean = false; public filter?: boolean = true ...

Angular auto suggest feature

I am working with a dropdown in Angular that contains JSON data. The data is stored in a List named options and I need to display the name field in the dropdown list. My current task involves implementing an autocomplete search feature for this dropdown. ...

Error: Unable to access the 'https' property as it is undefined

My nuxt app, which is built with Firebase and Vue, encounters an error when running the emulator. The error message states: TypeError: Cannot Find Property 'https' of undefined. This issue seems to be related to the https property in my index.ts ...

The code inside the promise .then block is executing long before the promise has completed its

After spending quite some time working on this messy code, I finally have a functioning solution: loadAvailabilities() { let promises = []; let promises2 = []; let indexi = 0; //return new Promise((resolve, reject) => { this.appo ...

ESLint refuses to be turned off for a particular file

I am in the process of creating a Notes.ts file specifically for TypeScript notes. I require syntax highlighting but do not want to use eslint. How can I prevent eslint from running on my notes file? Directory Structure root/.eslintignore root/NestJS.ts r ...

Using TypeScript conditional types with extends keyof allows for checking against specific keys, but it does not grant the ability

In TypeScript, I created custom types using template literal types to dynamically type the getters and setters of fields. The types are structured like this (Playground Link): export type Getters<T> = { [K in `get${Capitalize<keyof T & strin ...

Error: useRef in TypeScript - cannot be assigned to LegacyRef<HTMLDivElement> type

Struggling to implement useRef in TypeScript and facing challenges. When using my RefObject, I need to access its property current like so: node.current. I've experimented with the following approaches: const node: RefObject<HTMLElement> = us ...

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 ...

What is the correct way to handle the return value of an useAsyncData function in Nuxt 3?

How can I display the retrieved 'data' from a useAsyncData function that fetches information from a pinia store? <script setup lang="ts"> import { useSale } from "~/stores/sale"; const saleStore = useSale(); const { da ...

Ways to prevent scrolling in Angular 6 when no content is available

I am developing an angular 6 application where I have scrollable divs containing: HTML: <button class="lefty paddle" id="left-button"> PREVIOUS </button> <div class="container"> <div class="inner" style="background:red">< ...

Phaser 3 game app on iOS generated with Capacitor lacks audio functionality

I have developed a basic test app using Phaser 3 (written in Typescript and transpiled with rollup) and am utilizing Capacitor to convert it into an iOS application on my Mac. This excerpt highlights the key functionality of the app: function preload () { ...

Leveraging TypeScript's declaration file

Greetings! I am currently facing an issue while utilizing a declaration file in my TypeScript project. Here is the declaration file that I am working with: // Type definitions for Dropzone 4.3.0 // Project: http://www.dropzonejs.com/ // Definitions ...

Error message: Issue with transmitting system props in MUI v5 (Styled Components with TypeScript)

Recently delving into the world of Material UI, I encountered an issue: import React from 'react'; import { styled, Typography } from '@mui/material'; interface DescriptionProps { textTitle: string; type?: string; } const TitleSty ...

Incorporating a skeletal design effect into a table featuring sorting and pagination options

Is there a way to implement the Skeleton effect in a MUI table that only requires sorting and pagination functionalities? I tried using the 'loading' hook with fake data fetching and a 3-second delay, but it doesn't seem to work with DataGri ...