What's the best way to divide a TypeScript class into separate files?

While exploring ways to split a module into multiple files, I encountered various examples and even experimented with it myself. I found this method very useful. However, I also realized that it can be practical to split a class for the same reason. For instance, when dealing with several methods and wanting to avoid cramming everything into one lengthy file.

I am interested in finding something akin to the partial declaration in C#.

Answer №1

Recently, I've been following this new coding pattern:

// file class.ts
import { getValue, setValue } from "./methods";

class SuperClass {
    public getValue = getValue;
    public setValue = setValue;

    protected data = "some-data";
}
// file methods.ts
import { SuperClass } from "./class";

function getValue(this: SuperClass) {
    return this.data;
}

function setValue(this: SuperClass, value: string ) {
   this.data = value;
}

This approach allows us to separate our methods into a different file. However, there is now a circular dependency between class.ts and methods.ts. Despite how it may sound alarming, as long as the code execution remains non-circular, everything works smoothly. In this scenario, the methods.ts file does not execute any code from the class.ts. No problem!

You can also implement this pattern with a generic class like so:

class SuperClass<T> {
    public getValue = getValue;
    public setValue = setValue;

    protected data?: T;
}

function getValue<T>(this: SuperClass<T>) {
    return this.data;
}

function setValue<T>(this: SuperClass<T>, value: T) {
    this.data = value;
}

Answer №2

It is not possible to implement partial classes in TypeScript.

A feature request was made for partial classes on both CodePlex and GitHub, but it was later deemed out-of-scope as of April 4, 2017. The decision was based on the desire to stay aligned with ES6 standards and avoid adding more TypeScript-specific class features. The reasoning being that introducing additional TS-specific features may complicate the language further. Any scenario advocating for partial classes would need to justify itself through the TC39 process.

Answer №3

Here's a solution that I've used successfully with TypeScript version 2.2.2:

class MasterClass implements PartOne, PartTwo {
    // Only public members can be accessed in the class parts
    constructor(public message: string) { }

    // Assigning methods from prototypes to avoid accessibility issues
    methodOne = PartOne.prototype.methodOne;
    methodTwo = PartTwo.prototype.methodTwo;
}

class PartOne {
    methodOne(this: MasterClass) {
        return this.methodTwo();
    }
}

class PartTwo {
    methodTwo(this: MasterClass) {
        return this.message;
    }
}

Answer №4

When it comes to converting large, old JavaScript classes that utilize 'prototype' across multiple files into TypeScript, I opt for plain subclassing:

bigclassbase.ts:

class BigClassBase {
    methodOne() {
        return 1;
    }

}
export { BigClassBase }

bigclass.ts:

import { BigClassBase } from './bigclassbase'

class BigClass extends BigClassBase {
    methodTwo() {
        return 2;
    }
}

By doing this, you can import BigClass into any other TypeScript file.

Answer №5

Implementing modules allows for expanding a typescript class from another source file:

user.ts

export class User {
  name: string;
}

import './user-talk';

user-talk.ts

import { User } from './user';

class UserTalk {
  talk (this:User) {
    console.log(`${this.name} says relax`);
  }
}

User.prototype.sayHi = UserTalk.prototype.sayHi;

declare module './user' {
  interface User extends UserTalk { }
}

Example of how to use this implementation:

import { User } from './user';

const u = new User();
u.name = 'Frankie';
u.talk();
> Frankie says relax

If dealing with numerous methods, consider implementing the following approach:

// user.ts
export class User {
  static extend (cls:any) {
    for (const key of Object.getOwnPropertyNames(cls.prototype)) {
      if (key !== 'constructor') {
        this.prototype[key] = cls.prototype[key];
      }
    }
  }
  ...
}

// user-talk.ts
...
User.extend(UserTalk);

An alternative is to include the subclass in the prototype chain:

...
static extend (cls:any) {
  let prototype:any = this;
  while (true) {
    const next = prototype.prototype.__proto__;
    if (next === Object.prototype) break;
    prototype = next;
  }
  prototype.prototype.__proto__ = cls.prototype;
}

Answer №6

A revised iteration of the suggested design.

// modified.ts contents
import {fetchValue, setNewValue} from "./modified2";

export class LargeClass {
    // @ts-ignore - to bypass TS2564: Property 'fetchValue' has no initializer and is not definitely assigned in the constructor.
    public fetchValue:typeof fetchValue;

    // @ts-ignore - to bypass TS2564: Property 'setNewValue' has no initializer and is not definitely assigned in the constructor.
    public setNewValue:typeof setNewValue;
    protected data = "a-data";
}

LargeClass.prototype.fetchValue = fetchValue;
LargeClass.prototype.setNewValue = setNewValue;

//======================================================================
// modified2.ts contents
import { LargeClass } from "./modified";

export function fetchValue(this: LargeClass) {
    return this.data;
}

export function setNewValue(this: LargeClass, newData: string ) {
    this.data = newData;
}

Advantages

  • No extra fields created in class instances resulting in minimal overhead: no additional memory consumption during construction or destruction. Type declarations are purely for TypeScript typings without impacting JavaScript runtime.
  • Satisfactory IntelliSense functionality (validated on Webstorm)

Drawbacks

  • Use of ts-ignore directive is necessary
  • Syntax may appear less elegant compared to @Elmer's approach

The remaining characteristics of both solutions remain consistent.

Answer №7

If you want to organize your code into multiple files, you can implement multi file namespaces.

Create a file named Validation.ts with the following content:

namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }
}

In another file named LettersOnlyValidator.ts (which uses the StringValidator from Validation.ts), add the following code:

/// <reference path="Validation.ts" /> 
namespace Validation {
    const lettersRegexp = /^[A-Za-z]+$/;
    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }
}

Lastly, in the Test.ts file (which uses both StringValidator and LettersOnlyValidator from the previous files), include the following code:

/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />

// Some samples to try
let strings = ["Hello", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["Letters only"] = new Validation.LettersOnlyValidator();

Answer №8

Instead of creating a new function, why not utilize the built-in Function.call that JavaScript already provides.

class-a.ts

Class ClassA {
  bitten: false;

  constructor() {
    console.log("Bitten: ", this.bitten);
  }

  biteMe = () => biteMe.call(this);
}

In another file, bite-me.ts

export function biteMe(this: ClassA) {
  // do some stuff
  // here this refers to ClassA.prototype

  this.bitten = true;

  console.log("Bitten: ", this.bitten);
}

// Implementation

const state = new ClassA();
// Bitten: false

state.biteMe();
// Bitten: true

To learn more about how Function.call works, refer to the documentation on MDN.

Answer №9

Personally, I find the use of the @partial decorator to be a helpful way to simplify syntax and organize functionality within a single class by splitting it into multiple 🍬🍬🍬 class files. You can learn more about it here: https://github.com/mustafah/partials

Answer №10

// Here we have a declaration file for a BigClass with a method that takes a number and a string as arguments and returns a string

class BigClass {
  declare method: (n: number, s: string) => string;
}

// Now in the implementation file, we provide the actual implementation for the method
BigClass.prototype.method = function (this: BigClass, n: number, s: string) {
  return '';
}

One downside of this way of organizing code is that there is a risk of declaring a method without providing an actual implementation for it.

Answer №11

One way to incrementally enhance class methods is by utilizing both the prototype and Interface definitions:

import signIn from './signIn';
import browseStaff from './browse-staff';

interface EmployeeAPI {
  signIn(this: EmployeeAPI, data: SignInData): Promise<boolean>;
  browseStaff(this: EmployeeAPI): Promise<BrowseStaffResponse>;
}

class EmployeeAPI {
  // class implementation
}

EmployeeAPI.prototype.signIn = signIn;
EmployeeAPI.prototype.browseStaff = browseStaff;

export default EmployeeAPI;

Answer №13

Building on @Elmer's solution, I made some modifications to make it functional in a separate file.

util-services.ts

import { UtilityService } from "./utility-service";

export function performTask(this: UtilityService) {
...
}

utility-service.ts

import * as util from './util-services';

@Injectable({
    providedIn: 'root'
})
export class UtilityService {

    performTask = util.performTask;  // utilizing helper function for task completion

    public completeTask() {
        var result = this.performTask();
    }

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

What is the best way to test TypeScript optional parameter in Jasmine?

I am currently revising a TypeScript function to include an optional parameter with a default value. This function is a crucial operation, and it is utilized by several high-level operations. Existing calls to the function do not include the new parameter ...

Remove array element by key (not numerical index but string key)

Here is a JavaScript array that I am working with: [#ad: Array[0]] #ad: Array[0] #image_upload: Array[0] 13b7afb8b11644e17569bd2efb571b10: "This is an error" 69553926a7783c27f7c18eff55cbd429: "Yet another error" ...

Send certain attributes from the main component to the subcomponent

When dealing with components, I often find myself needing to manipulate specific children based on their type. For example, in the code snippet below, I want to set additional props on a child component if it matches a certain type: import React from &apo ...

Tips on hiding the checkbox within the Material UI TextField input when using MenuItems with checkboxes

I've customized the MenuItems in a TextField of type Select to include checkboxes. However, when I select an item from the menu, the checkbox is also displayed in the input field of the TextField. Requirement: I want to hide the checkbox in the TextF ...

Smooth scrolling feature malfunctioning in mobile view

While working on my website, I noticed that the smooth-scroll feature works perfectly on desktop browsers. However, on mobile devices, when I click on a link, it does not scroll to the correct position. It always ends up much lower than expected. Any idea ...

Managing the accumulation of response chunks in a streaming request with Axios

I have a proxy server that needs to make a request to an external API server to synthesize a voice from some text. According to the API docs, I will first receive a response with headers and then stream binary data, as the response body contains 'Tran ...

Creating a unique CSS animation featuring a vertical line intersecting a horizontal line

Check out this cool expanding line animation I created using CSS, see it in action here I'm looking for assistance on how to add vertical lines at both ends once the animation stops. Something similar to the image shown below: https://i.sstatic.net/ ...

Leveraging the Angular (2) routerLinkActive directive to handle dynamic routes

Although my current approach works, I believe there may be a more efficient way to implement this in Angular. The situation is as follows: Imagine nested, inflected paths like /logos and /logo/:id The markup below functions as intended: <li class ...

Error: The JSON input unexpectedly ended, however the PHP file itself is error-free

When trying to display data retrieved using PHP through JSON/Ajax, I encountered an error message: [object Object] | parsererror | SyntaxError: Unexpected end of JSON input The PHP script is functional (I can view the JSON output by directly accessing th ...

Passing boolean values to component attributes in Vue.js

I am attempting to create a straightforward input component using Vue, where if the condition IsPassword is true, the type will be set to "password," and if it is false, the type will be set to "text." I suspect there may be a syntax error causing a pars ...

Issue TS1112: It is not possible to declare a class member as optional

I'm currently working on creating a movie catalog using Angular and Ionic. Within the Movie class, I have properties for id, title, image, and plot. On the initial page of the app, only the id, title, and image are displayed, while the plot is omitte ...

Updating a key's value within a mapped object using React from a child component

When working with React, I successfully updated a value of an object stored in the parent component through an onChange() event triggered in a child component. However, I encountered a challenge when trying to achieve the same result with a component crea ...

Having trouble submitting a date input form generated with vuejs on Safari browser

I am a huge fan of Vuejs and it's my go-to at work. The other day, I came across a rather perplexing scenario. If you have any insights into this, please do share. Here is the code in question: <script setup lang="ts"> import { ref ...

Using Vue 2 with a personalized Axios setup, integrating Vuex, and incorporating Typescript for a robust

I'm still getting the hang of Typescript, but I'm facing some challenges with it when using Vuex/Axios. Current setup includes: Vue CLI app, Vue 2, Vuex 3, Axios, Typescript At a high level, I have a custom Axios instance where I configure the ...

When using jQuery, the content loaded with the $ajax function only displays after refreshing the page

Located on an external server is a directory containing various .html files, including one named index.html. The server has the ability to load either the folder name or foldername/index.html in its URL. Each html file within the directory loads a corresp ...

Modify the form's action attribute when submitted

I am trying to create a form with multiple buttons that will change the action and target properties when a specific button is clicked. Below is my code snippet: <div id="root"> <form :action="form.action" ref="form" :target="form.target"&g ...

What is the process for creating a jQuery object in TypeScript for the `window` and `document` objects?

Is there a way to generate a jQuery object in TypeScript for the window and document? https://i.stack.imgur.com/fuItr.png ...

The visual representation of my data in Highchart appears sporadic, with the points scattered rather than following a clean, linear progression from left to right

I am working on displaying a 2D array [timestamp, count] on a highchart for the past 90 days from left to right. However, I am facing an issue where the chart appears sporadic when introduced to production data. It works fine with smaller datasets. After ...

What is the best way to pass a JSON object from R to Plumber in a format that JavaScript can interpret as an array instead of

My goal is to receive a JSON raw response from R Plumber and then utilize it in Angular. However, I am encountering an issue where the JavaScript Framework is interpreting it as a string instead of recognizing it as JSON format. "[{\"id&bsol ...

Monitoring of access controls on Safari during uploads to S3

Safari 10.1.2 Encountering an issue intermittently while attempting to upload PDF files to S3 using a signed request with the Node aws-sdk. Despite working smoothly 90% of the time, have been pulling my hair out trying to resolve this problem. Could it be ...