hckr.fyi // thoughts

Extending Strings (and other Data Types) in TypeScript

by Michael Szul on

I mentioned in my last post that I'm rewriting metron from the ground up. The original version of metron was layered with cross-browser compatibility code, convenience functions, and--important to this post--extensions to JavaScript data types and native types.

Now, many programmers and programming pundits today complain that extending types is a bad idea, and prefer to use some sort of wrapper or straight function, but I disagree. When I code, I tend to know the full extent of my application, so I don't worry about overwriting other functions attached to the objects prototypes, and adding on these extensions makes things more convenient, and proofs out layers of extensibility in object-oriented programming. Ultimately, if you don't like it, don't use the library; plain and simple, but as always, you should research your code base to make sure you don't create any side effects.

There are, however, many programmers searching Google and Stack Overflow for ways to extend JavaScript, as well as its superset TypeScript.

My rewrite of metron is in TypeScript, so this post is going to show the basics of extending types in that language. TypeScript merges interface declarations, so you can actually define an interface in one part of the code, and later define it again, and TypeScript will merge the definitions. This is how extending JavaScript types works: basically TypeScript has already declared an interface for the data type, and you declare it again, adding in your methods. Take a look at the metron.extenders.ts file on GitHub (partial code below):

interface String {
        lower: () => string;
        upper: () => string;
        ltrim: () => string;
        rtrim: () => string;
        normalize: () => string;
        startsWith: (part: string) => boolean;
        endsWith: (part: string) => boolean;
        capFirst: () => string;
        capWords: () => string;
        truncateWords: (number: number) => string;
        truncateWordsWithHtml: (number: number) => string;
        stripHtml: () => string;
        escapeHtml: () => string;
        toBool: () => boolean;
        contains: (val: string) => boolean;
    }
    

This a TypeScript interface containing methods that don't exist on the JavaScript string data type. Later we define the methods on the prototype. For example, here is the definition for normalize():

String.prototype.normalize = function (): string {
        return this.replace(/^\s*|\s(?=\s)|\s*$/g, "");
    };
    

What about an essentially static method? TypeScript actually has a definition for String as a variable of type StringConstructor, and you can extend the StringConstructor interface in the same way as above.

interface StringConstructor {
        isNullOrEmpty: (val: any) => boolean;
    }
    

isNullOrEmpty() is defined in the StringConstructor interface as expected, but you define the method on the String object:

String.isNullOrEmpty = function (val: any): boolean {
        if (val === undefined || val === null || val.trim() === '') {
            return true;
        }
        return false;
    };
    

This allows you to call something like:

if(String.isNullOrEmpty(variable)) {
        //Do something
    }
    

Best practices should probably call for all extension methods to be declared off of the StringConstructor interface, but the methods at the beginning of this post were from the old metron library. They've been converted to TypeScript, but I haven't rearranged any definitions as of yet. In either event, when the TypeScript is transpiled to JavaScript, it creates extensions in the same way, regardless of whether defined on String or StringConstructor (it results in the same output); it's only the TypeScript philosophy that differs.

None of this is limited to just strings, you can extend numbers, dates, elements, and even the document native type for additional DOM methods. In fact, the rewrite of metron is extending a lot of these elements to produce a more convenient and readable source. Just remember that to utilize number extensions, you'll have to wrap the number in parentheses or use the .. notation:

(1).toBool();
    //or
    1..toBool();