105

I found a lot of examples and also tried myself to split a module into several files. So I get that one, very handy. But it's also practical sometimes to split a class for the same reason. Say I have a couple of methods and I don't want to cram everything into one long file.

I'm looking for something similar to the partial declaration in C#.

Adrian Rosca
  • 6,922
  • 9
  • 40
  • 57
  • 3
    IMO, it doesn't make sense to split something that should represent a [single responsibility](http://en.wikipedia.org/wiki/Single_responsibility_principle) into multiple files. The `partial` C# declaration really shines when one partial is auto-generated code, but this won't be the case with your TypeScript, right? Is your class taking on too many roles? Could you turn it >1 classes? – spender May 26 '14 at 19:50
  • 2
    You have a point. Maybe I could just break all methods into a module that could act as a helper instead of having helper methods on the class. I'll consider that. – Adrian Rosca May 26 '14 at 21:09
  • @spender I tried your suggestion. And I must say it only "worked" partially. Some methods does clearly belong outside the class, but some metods are really logical to have inside the class. Primarily data handling methods that sorts, filters and somehow deals with the data inside the class. So I once again ended up with having quite a large class file that sure would have been more convenient to split up in several files. I cannot see how it would be more logical to put those methods in other classes. It just fragments the code as I see it. But I don't say I'm neccessarily right... =) – Adrian Rosca May 28 '14 at 08:44
  • 4
    I disagree with the first comment. I have found many times that partial classes separated across many files are very useful. this is true primarily when you have a team of developers working together on the same class. – Nathan Oct 04 '18 at 14:32
  • 4
    @spender: It's also helpful when you want a class that is partially written by hand and partially by a code generator. – Joshua Frank Mar 20 '19 at 12:36
  • I solved this with regular JS here: https://stackoverflow.com/a/62142995/1599699 – Andrew Jun 02 '20 at 00:28
  • 1
    I'm here because my class (pretty simple CRUD) is almost 1k lines because of overloads and type definitions. Ouch. – Cory Mawhorter Jan 20 '21 at 03:36
  • 1
    @Nathan what class is big enough for multiple devs to work on it at the same time? when you hit above 500 lines of code for a single class, it is time to think about refactoring it. That is not always possible, but much more often than devs want to admit that it is. just create new layers by outsourcing code that belongs together in subclasses. makes working with multiple devs and git much easier as well. – Welcor Feb 01 '23 at 17:23

13 Answers13

92

Lately I use this pattern:

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

class BigClass {
    public getValue = getValue;
    public setValue = setValue;

    protected value = "a-value";
}
// file methods.ts
import { BigClass } from "./class";

function getValue(this: BigClass) {
    return this.value;
}

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

This way we can put methods in a seperate file. Now there is some circular dependency thing going on here. The file class.ts imports from methods.ts and methods.ts imports from class.ts. This may seem scary, but this is not a problem. As long as the code execution is not circular everything is fine and in this case the methods.ts file is not executing any code from the class.ts file. NP!

You could also use it with a generic class like this:

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

    protected value?: T;
}

function getValue<T>(this: BigClass<T>) {
    return this.value;
}

function setValue<T>(this: BigClass<T>, value: T) {
    this.value = value;
}
Yves M.
  • 29,855
  • 23
  • 108
  • 144
Elmer
  • 9,147
  • 2
  • 48
  • 38
  • 5
    This looks like a fantastic solution I'm going to give it a try with my team on our next project! – Nathan Oct 04 '18 at 14:38
  • 5
    @Elmer This works great! For anyone curious, it's documented [here](https://www.typescriptlang.org/docs/handbook/functions.html) (`this` parameters) – nachocab Dec 01 '18 at 09:19
  • 4
    Nice solution, the only problem I've found is that you can't call the class private methods within the external funcion. – Galactus Sep 10 '20 at 23:19
  • 1
    @Galactus that's right! but you can access protected members! It's not the same but it could be a (maybe dirty) solution. – Elmer Sep 15 '20 at 11:04
  • How would I use this if `BigClass` has type arguments? e.g. `BigClass` – bmarti44 Oct 14 '20 at 22:39
  • I am a bit confused, you need to import the BigClass (file) in the methods' file. So, the methods' file depends on the BigClass's file. Also, you need to import the method's file in the BigClass to be able to use them. So, the BigClass depends on the methods' file. How you would load these files when there is a loop. Unless you use an abstraction. Am I wrong? – Hamid Mayeli Oct 22 '20 at 09:32
  • @HamidMayeli You are right! So you would have a method file that imports from a class file, the class file imports from the method file. This is circular! However, this will not cause any problems in as long as there is no circular execution of code. This is not the case here. – Elmer Oct 22 '20 at 10:39
  • 1
    @bmarti44 That is a good question! I actually never tried it with a generic class, after some fiddeling I updated the answer! – Elmer Oct 22 '20 at 10:42
  • 1
    @Elmer that works great! It seems I would need to refactor the functions (if they already existed without a type) to accept a type argument (but it looks like it's not a huge deal for my use case). – bmarti44 Oct 22 '20 at 17:44
  • 4
    @Elmer, this approach seems great, but in fact it has a big drawback: it actually creates additional fields in the class instead of using prototypes for that. This might greatly increase memory usage and will make the code much slower if you have many instances of that class, also creation and deletion of such instances might be slower. So this approach seems to be applicable if perfomance isn't an issue. – Yuri Yaryshev Dec 25 '20 at 08:11
  • @YuriYaryshev I would love to see some numbers on that, or is all of this based on assumption? Also, I would not choose js if performance is critical. – Elmer Dec 25 '20 at 21:15
  • Love this solution! A minor limitation is that functions lose their doc strings that are defined in `methods.ts`. – Jay Wang Mar 08 '22 at 23:35
  • 2
    Regarding the circular imports, it may be clearer to use `import type { BigClass } ...` to import the type alone, which makes it more clear that there isn't a circular dep on the implementations. – nishanthshanmugham Mar 31 '22 at 12:20
39

You can't.

There was a feature request to implement partial classes, first on CodePlex and later on GitHub, but on 2017-04-04 it was declared out-of-scope. A number of reasons are given, the main takeaway seems to be that they want to avoid deviating from ES6 as much as possible:

TypeScript already has too many TS-specific class features [...] Adding yet another TS-specific class feature is another straw on the camel's back that we should avoid if we can. [...] So if there's some scenario that really knocks it out of the park for adding partial classes, then that scenario ought to be able to justify itself through the TC39 process.

user247702
  • 23,641
  • 15
  • 110
  • 157
  • 53
    If the goal of typescript is to remain close to ES6, why does it exist? We already have ES6 – Jose Dec 27 '18 at 18:47
  • 6
    @Jose TypeScript was publicly released in October 2012, nearly 3 years before the ES6 spec was finalised. It was a very different landscape then, writing JavaScript in an organised way was quite challenging. TypeScript wasn't the only alternative language, we had CoffeeScript and Dart for example. Since then, JavaScript has evolved a lot, in part thanks to those projects. Today, the most significant feature that TypeScript provides is static typing. – user247702 Dec 27 '18 at 18:59
  • 1
    @Stijn Totally agree. But we have Flow for that now. ES# + Flow beats Typescript at every turn by far, and transpiles rather than compile! I believe it is lovely that Typescript is out there to provide inspiration and competition, so that Babel and Flow keep getting better. But I'll never choose that mammoth of a thing over a much better performance, more elegant and as feature-rich ES# + Flow. Would you?? – Edoardo L'Astorina Feb 12 '19 at 16:23
  • @EdoardoL'Astorina I haven't done a lot of frontend development during the past couple years, in fact it's the first time I hear about Flow so I can't comment on it. But I'm not surprised that alternatives have appeared. – user247702 Feb 12 '19 at 16:43
  • @Jose ES6 (and TS for that matter) is an attempt to turn JS into an OOP. It is not an OOP, it is LISP with C-like syntax. The data types are LISP data types, the implicit type conversions are LISP behaviour and the closures are LISP closures. If you understand LISP, the behaviour of `this` isn't even slightly surprising, what's _weird_ is all the pretense of OOP. The people who created TS and ES6 probably _did_ understand this, and they were trying to re-make the language to match the misperceptions of its users and validate the invalid. – Peter Wone Jun 08 '21 at 06:50
  • "they want to avoid deviating from ES6 as much as possible". Heaven forbid we have an enterprise-class programming language for the web. – Emperor Eto Sep 26 '21 at 12:21
28

I use this (works in typescript 2.2.2):

class BigClass implements BigClassPartOne, BigClassPartTwo {
    // only public members are accessible in the
    // class parts!
    constructor(public secret: string) { }

    // this is ugly-ish, but it works!
    methodOne = BigClassPartOne.prototype.methodOne;
    methodTwo = BigClassPartTwo.prototype.methodTwo;
}

class BigClassPartOne {
    methodOne(this: BigClass) {
        return this.methodTwo();
    }
}

class BigClassPartTwo {
    methodTwo(this: BigClass) {
        return this.secret;
    }
}
Elmer
  • 9,147
  • 2
  • 48
  • 38
  • 1
    Would this "prototyping" workaround kills the intellisense? – JeeShen Lee May 10 '17 at 04:22
  • 1
    No! It's intellisense friendly (at least in vscode) – Elmer May 11 '17 at 09:10
  • 2
    You should make the methods of the parts static (`static methodOne(this: BigClass) {...}`) to get rid of `.prototype`. –  Jul 31 '19 at 17:09
  • this -> https://stackoverflow.com/questions/23876782/how-do-i-split-a-typescript-class-into-multiple-files/52393598#52393598 is a better solution! – Elmer Oct 22 '20 at 10:51
  • @Elmer, this approach seems great, but in fact it has a big drawback (same for the one above): it actually creates additional fields in the class instead of using prototypes for that. This might greatly increase memory usage and will make the code much slower if you have many instances of that class, also creation and deletion of such instances might be slower. So this approach seems to be applicable if perfomance isn't an issue. – Yuri Yaryshev Dec 25 '20 at 08:12
  • @YuriYaryshev you're right, even though it is very unlikely that this will ever be a real problem, it will result in some overhead! I posted a different solution that is more efficient! – Elmer Dec 25 '20 at 21:11
  • @user11104582 I also realized that by using static methods, you can't cross-reference the methods within the same class, e.g. you can't call methodOne within methodTwo if both were declared within BigClassOne otherwise you get a "Property ABC does not exist on type XYZ" error. – Albert Sun Jul 30 '21 at 19:53
11

I use plain subclassing when converting large old multi file javascript classes which use 'prototype' into multiple typescript files:

bigclassbase.ts:

class BigClassBase {
    methodOne() {
        return 1;
    }

}
export { BigClassBase }

bigclass.ts:

import { BigClassBase } from './bigclassbase'

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

You can import BigClass in any other typescript file.

masp
  • 350
  • 3
  • 10
4

A modified version of proposed pattern.

// temp.ts contents
import {getValue, setValue} from "./temp2";

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

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

BigClass.prototype.getValue = getValue;
BigClass.prototype.setValue = setValue;

//======================================================================
// temp2.ts contents
import { BigClass } from "./temp";

export function getValue(this: BigClass) {
    return this.value;
}

export function setValue(this: BigClass, value: string ) {
    this.value = value;
}

Pros

  • Doesn't create additional fields in class instances so there is no overhead: in construction, destruction, no additional memory used. Field declations in typescript are only used for typings here, they don't create fields in Javascript runtime.
  • Intellisence is OK (tested in Webstorm)

Cons

  • ts-ignore is needed
  • Uglier syntax than @Elmer's answer

The rest properties of solutions are same.

Yuri Yaryshev
  • 991
  • 6
  • 24
  • I wonder why this is not more upvoted. As far as I understand it it is the best solution because it actually answers the OPs question how to accieve "something similar to the partial declaration in C#". All other solutions add functions to the instances instead of the prototype of the class. This is the solution I use. – wizard23 Jul 10 '22 at 23:14
  • Add `declare` and no ugli code any more. `declare public setValue: typeof setValue;`. But additional con to you solution it is not possible to prototype private and protected methods. – Yuriy Gyerts Apr 24 '23 at 17:27
4

Modules let you extend a typescript class from another 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 { }
}

Usage:

import { User } from './user';

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

If you have a lot of methods, you might try this:

// 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);

Or add the subclass to 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;
}
bendytree
  • 13,095
  • 11
  • 75
  • 91
  • This seems to work really well with no performance hit and the ability to use `protected` class members. Here's a demo: https://codepen.io/cjbarth/pen/XWEggwV – cjbarth Jul 22 '22 at 17:27
2

You can use multi file namespaces.

Validation.ts:

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

LettersOnlyValidator.ts (uses the StringValidator from above):

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

Test.ts (uses both StringValidator and LettersOnlyValidator from above):

/// <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();
Hypenate
  • 1,907
  • 3
  • 22
  • 38
2

Why not just use Function.call that js already comes with.

class-a.ts

Class ClassA {
  bitten: false;

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

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

and in other 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);
}

// using it

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

state.biteMe();
// Bitten: true

For more information have a look at the definition of Function.call

Rushi patel
  • 522
  • 7
  • 17
  • This approach seems great, but in fact it has a big drawback: it actually creates additional fields in the class instead of using prototypes for that. This might greatly increase memory usage and will make the code much slower if you have many instances of that class, also creation and deletion of such instances might be slower. So this approach seems to be applicable if perfomance isn't an issue. – Yuri Yaryshev Dec 25 '20 at 08:13
1

Personally I use @partial decorator acts as a simplified syntax that may help divide functionality of a single class into multiple class files ... https://github.com/mustafah/partials

Mustafah
  • 4,447
  • 2
  • 24
  • 24
1
// Declaration file
class BigClass {
  declare method: (n: number, s: string) => string;
}

// Implementation file
BigClass.prototype.method = function (this: BigClass, n: number, s: string) {
  return '';
}

The downside of this approach is that it is possible to declare a method but to forget to actually add its implementation.

Jorge Galvão
  • 1,729
  • 1
  • 15
  • 28
0

We can extend class methods gradually with prototype and Interface definition:

import login from './login';
import staffMe from './staff-me';

interface StaffAPI {
  login(this: StaffAPI, args: LoginArgs): Promise<boolean>;
  staffsMe(this: StaffAPI): Promise<StaffsMeResponse>;
}

class StaffAPI {
  // class body
}

StaffAPI.prototype.login = login;
StaffAPI.prototype.staffsMe = staffsMe;

export default StaffAPI;
Masih Jahangiri
  • 9,489
  • 3
  • 45
  • 51
0

This is how i have been doing it Mixins approach

Pranay Dutta
  • 2,483
  • 2
  • 30
  • 42
-1

To add to @Elmer's solution, I added following to get it to work in separate file.

some-function-service-helper.ts

import { SomeFunctionService } from "./some-function-service";

export function calculateValue1(this: SomeFunctionService) {
...
}

some-function-service.ts

import * as helper from './some-function-service-helper';

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

    calculateValue1 = helper.calculateValue1;  //  helper function delcaration used for getNewItem

    public getNewItem() {
        var one = this.calculateValue1();
    }
SamJackSon
  • 1,071
  • 14
  • 19
  • This approach seems great, but in fact it has a big drawback (same as the for ones above): it actually creates additional fields in the class instead of using prototypes for that. This might greatly increase memory usage and will make the code much slower if you have many instances of that class, also creation and deletion of such instances might be slower. So this approach seems to be applicable if perfomance isn't an issue. – Yuri Yaryshev Dec 25 '20 at 08:14