Smartboy
Smartboy

Reputation: 692

How to use lodash.mixin in TypeScript

My team is evaluating switching some of our files to TypeScript from JavaScript, and we make extensive use of some custom mixin methods in our code. From doing some basic tests, it seems that while we can use _.mixin to create mixins as per the norm, we cannot reference them without getting a compilation error. Of course, we could put these references in the definition file, but I usually prefer not to modify that.

Is there any way to do what we're looking for, or are we out of luck?

Upvotes: 8

Views: 6972

Answers (6)

iGoodie
iGoodie

Reputation: 162

On top of @smartboy's answer, you actually don't need to declare types and the actual functions apart from each other. That's how you can create your mixins, and infer the types directly from them:

// util/lodash.mixins.ts

import _ from 'lodash';

class LoDashMixins {
  isStringEmpty(str: string) {
    return str.length === 0;
  }
  sum(a: number, b: number) {
    return a * b;
  }
  yourFunction() {
    return 'foo' as const;
  }
}

/* -------------------------- */

// This one binds your mixin functions
_.mixin(
  Object.getOwnPropertyNames(Mixins.prototype).reduce((obj, name) => {
    obj[name] = Mixins.prototype[name];
    return obj;
  }, {}),
);

// And this one mixes in your types!
declare module 'lodash' {
  interface LoDashStatic extends LoDashMixins {}
}

As shown above, you only have to declare your functions once. Classes are a good way to cover both runtime entities and typescript types for such cases

Upvotes: 0

Chris
Chris

Reputation: 445

See TypeScript docs on extending built-in types, which I think applies here as well. _ is defined as var _: _.LoDashStatic, and vars are not currently extendable.

The best way that I found to expose extensions is via a lodash-mixins.ts script that defines a new LoDashMixins interface (extending LoDashStatic), applies the mixins, and exports _ cast to the custom interface. This example defines a single mixin, but the idea is to add all your mixins to one script for easy importing.

import * as _ from 'lodash';
import xdiff from './xdiff';

interface LoDashMixins extends _.LoDashStatic {
  xdiff<T>(array:T[], values:T[]): T[];
}

_.mixin({xdiff: xdiff});

export default _ as LoDashMixins;

When you want to use the mixins, import './lodash-mixins' instead of 'lodash'. You now have compile-time visibility to all of the built-in functions, as well as your mixins.

import _ from './lodash-mixins';

_.map([]); // built-in function still compiles
_.xdiff([], []); // mixin function compiles too

Upvotes: 10

jfrazx
jfrazx

Reputation: 31

I found the docs on module augmentation to be helpful. I used a combination of this and another answer.

// my-lodash.ts
import * as _ from 'lodash';

declare module 'lodash' {
  interface LoDashStatic {
    isNonEmptyString(str: string): boolean;
    isEmptyString(str: string): boolean;
    isEmptyArray<T>(a: T[]): boolean;
    isNonEmptyArray<T>(a: T[]): boolean;
    isNullOrEmptyString(str: string): boolean;
    isNullOrUndefined<T>(val: T): boolean;
    isNullOrEmpty<T>(value: T[]): boolean;
    isNullOrEmpty<T>(value: Dictionary<T>): boolean;
    isNullOrEmpty<T>(value: T): boolean;
  }
}

module LoDash {
  export function isEmptyArray<T>(a: T): boolean {
    return Array.isArray(a) && !a.length;
  }
  // the rest of your functions
}

_.mixin(Object.keys(LoDash)
               .reduce(
                 (object, key) => {
                   object[key] = LoDash[key];
                   return object;
                 },
                 Object.create(null)
              )); 

export = _;

Doing it this way, you can avoid casting or using a default export, which means you can continue importing in the same fashion.

Now, in some other file, utilize your augmented module as such:

// another-file.ts
import * as _ from './my-lodash';

_.isEmptyArray([]);
=> true

Upvotes: 1

Blake Bowen
Blake Bowen

Reputation: 1049

You can do this.

// somewhere in your project
declare module _ {
    interface LoDashStatic {
        foo(value: string): number;
    }
}

// extend it somewhere else 
declare module _ {
    interface LoDashStatic {
        bar(value: number);
    }
}

Test it out

Upvotes: 6

Smartboy
Smartboy

Reputation: 692

For now, it looks like what I want isn't available without any pain. Instead, I have to modify the lodash.d.ts file to include the definitions that I want, similar to the following:

declare module _ {
    // Default methods declared here...

    //*************************************************************************
    // START OF MIXINS, THESE ARE NOT PART OF LODASH ITSELF BUT CREATED BY US!
    //*************************************************************************

    interface LoDashStatic {
        isNonEmptyString: (str: string) => boolean;
        isEmptyString: (str: string) => boolean;
        isEmptyArray: (a: any[]) => boolean;
        isNonEmptyArray: (a: any[]) => boolean;
        isNullOrEmptyString: (str: string) => boolean;
        isNullOrUndefined: (val: any) => boolean;
        isNullOrEmpty(value: any[]): boolean;
        isNullOrEmpty(value: _.Dictionary<any>): boolean;
        isNullOrEmpty(value: string): boolean;
        isNullOrEmpty(value: any): boolean;
    }

    //*************************************************************************
    // END OF MIXINS
    //*************************************************************************

    // Default types declared here...
}

I hate modifying the default files, but it seemed the lesser of the evils.

Upvotes: 1

user1998858
user1998858

Reputation:

You can do this using type erasure:

import _ = require('lodash');

_.mixin(require('lodash-deep'));

function deepSet(lodash: any, path: Array<string>, record: IFooRecord, 
        replacement: number): void { 
    lodash.deepSet(object, path, replacement); 
}

interface IBarRecord {
   bar: number;
}

interface IFooRecord {
   foo: IBarRecord;
}

var subject: IFooRecord = { 
   foo: {
      bar: 0
   }
};
var replacement: number = 1;

deepSet(_, ['foo', 'bar'], subject, replacement);

It's kind of a cludge, but your code will compile. You could also create your own proxy implementing the interface of the mixin and inject the lodash module instance into it to a achieve a more modular result:

import _ = require('lodash');

_.mixin(require('lodash-deep'));    

module 'lodash-deep' {

   export class lodashDeep {

     private _: any;

      constructor(lodash?: any) {
         if (!lodash) {
            lodash = _;
         }
         this._ = lodash;
      }

      public deepSet(collection: any, path: any, value: any): void {
         this._.deepSet(collection, path, value);
      }

      ...

   }

}

Upvotes: 1

Related Questions