98

I am extending String prototype chain with a new method but when I try to use it it throws me an error: property 'padZero' does not exist on type 'string'. Could anyone solve this for me?

The code is below. You can also see the same error in Typescript Playground.

interface NumberConstructor {
    padZero(length: number);
}
interface StringConstructor {
    padZero(length: number): string;
}
String.padZero = (length: number) => {
    var s = this;
    while (s.length < length) {
      s = '0' + s;
    }
    return s;
};
Number.padZero = function (length) {
    return String(this).padZero(length);
}
Rafael 'BSIDES' Pereira
  • 2,951
  • 5
  • 22
  • 24

6 Answers6

148

This answer applies to TypeScript 1.8+. There are lots of other answers to this sort of question, but they all seem to cover older versions.

There are two parts to extending a prototype in TypeScript.

Part 1 - Declare

Declaring the new member so it can pass type-checking. You need to declare an interface with the same name as the constructor/class you want to modify and put it under the correct declared namespace/module. This is called scope augmentation.

Extending the modules in this way can only be done in a special declaration .d.ts files*.

//in a .d.ts file:
declare global {
    interface String {
        padZero(length : number) : string;
    }
}

Types in external modules have names that include quotation marks, such as "bluebird".

The module name for global types such as Array<T> and String is global, without any quotes. However, in many versions of TypeScript you can forego the module declaration completely and declare an interface directly, to have something like:

declare interface String {
        padZero(length : number) : string;
}

This is the case in some versions pre-1.8, and also some versions post-2.0, such as the most recent version, 2.5.

Note that you cannot have anything except declare statements in the .d.ts file, otherwise it won't work.

These declarations are added to the ambient scope of your package, so they will apply in all TypeScript files even if you never import or ///<reference the file directly. However, you still need to import the implementation you write in the 2nd part, and if you forget to do this, you'll run into runtime errors.

* Technically you can get past this restriction and put declarations in regular .ts files, but this results in some finicky behavior by the compiler, and it's not recommended.

Part 2 - Implement

Part 2 is actually implementing the member and adding it to the object it should exist on like you would do in JavaScript.

String.prototype.padZero = function (this : string, length: number) {
    var s = this;
    while (s.length < length) {
      s = '0' + s;
    }
    return s;
};

Note a few things:

  1. String.prototype instead of just String, which is the String constructor, rather than its prototype.
  2. I use an explicit function instead of an arrow function, because a function will correctly receive the this parameter from where it's invoked. An arrow function will always use the same this as the place it's declared in. The one time we don't want that to happen is when extending a prototype.
  3. The explicit this, so the compiler knows the type of this we expect. This part is only available in TS 2.0+. Remove the this annotation if you're compiling for 1.8-. Without an annotation, this may be implicitly typed any.

Import the JavaScript

In TypeScript, the declare construct adds members to the ambient scope, which means they appear in all files. To make sure your implementation from part 2 is hooked up, import the file right at the start of your application.

You import the file as follows:

import '/path/to/implementation/file';

Without importing anything. You can also import something from the file, but you don't need to import the function you defined on the prototype.

GregRos
  • 8,667
  • 3
  • 37
  • 63
  • I am using v2.1.0-dev.20161001. I tried this before, but it gives me the error above in `declare global` too. Also, I noticed a lot of answers from stackoverflow tell us to extend without prototype but the code generated doesn't use it... what is the catch here? Why is this so hard? – Rafael 'BSIDES' Pereira Oct 06 '16 at 00:53
  • That's not the main thing wrong in your code above. The main thing is that you're extending the constructor, not the prototype. If you wrote something else, then update the code in the question. – GregRos Oct 06 '16 at 08:14
  • This works when I try it in a local project, but it doesn't work in Angular. Could Angular be overwriting the String again or something...? – Kokodoko Aug 03 '17 at 13:45
  • **Ambient Scope** was the issue for me - great catch! I had to import the prototype into the TS files I wanted to use the extensions in, it didn't use to work that way in earlier versions of TS – SliverNinja - MSFT Sep 05 '17 at 16:59
  • Doesn't seem to work in an Angular project, saying the method is not defined on String. if you have some extra tips on how to get this to work in an Angular project, please let us know. – MikeDub Oct 17 '17 at 01:25
  • this example is somewhat confusing and could be better if it were shown as complete. The import is specifically confusing. Do you declare the variable again? Or are you referencing the `declare` from part 1 – NSjonas Nov 07 '17 at 03:25
  • 9
    also, if you try to put it in a `.d.ts` file in 2.5.1 you get the following error: `Augmentations for the global scope can only be directly nested in external modules or ambient module declarations.` – NSjonas Nov 07 '17 at 03:26
  • also, also... there is no export in part 2 of your example? how do you import the function? – NSjonas Nov 07 '17 at 03:27
  • @NSjonas I can't reproduce that in 2.5.3. Maybe you have other stuff in the `.d.ts` file besides `declare` statements? There is no need to export anything. All you need to do is execute the code inside the implementation file. I made that a bit clearer. – GregRos Nov 07 '17 at 12:06
  • 1
    @GregRos Yea, I had other stuff in my `.d.ts` file. I put it in the root `types.d.ts` and it seems to work without the declare. Thanks for clarifying the import. – NSjonas Nov 07 '17 at 19:05
  • 1
    Hey this is a great answer, I was wondering if you know of any way to modify a global interface in a non-ambient context? – Patrick Roberts Jul 04 '18 at 19:42
  • 1
    @PatrickRoberts If the type declarations are inside a package, only an application that `import`s the package will be able to see them. Other than that, there isn't really a way to do it directly. But maybe there are alternatives. Open a question and explain what you mean. – GregRos Jul 09 '18 at 14:50
  • is there a way to wire it up without import the implementation file? – Liran May 13 '23 at 12:56
84

If you want to augment the class, and not the instance, augment the constructor:

declare global {
  interface StringConstructor {
    padZero(s: string, length: number): string;
  }
}

String.padZero = (s: string, length: number) => {
  while (s.length < length) {
    s = '0' + s;
  }
  return s;
};

console.log(String.padZero('hi', 5))

export {}

*The empty export on the bottom is required if you declare global in .ts and do not export anything. This forces the file to be a module. *

If you want the function on the instance (aka prototype),

declare global {
  interface String {
    padZero(length: number): string;
  }
}

String.prototype.padZero = function (length: number) {
  let d = String(this)
  while (d.length < length) {
    d = '0' + d;
  }
  return d;
};

console.log('hi'.padZero(5))

export {}
Steven Spungin
  • 27,002
  • 5
  • 88
  • 78
  • I've done the second option, but for some reason at runtime I still get isNullOrEmpty is not a function. – Jacques Oct 27 '20 at 15:58
  • thats not a typescript issue then. you should debug to see what runtime type it is. – Steven Spungin Oct 29 '20 at 06:05
  • 1
    Managed to figure out my mistake: I augmented the instance, which is kind of pointless when you're trying to test for Null OR Empty considering if the variable is null you'll end up with the exception I had. I changed it to augmenting the class which solved my issue. Thanks for the insight in your answer. – Jacques Oct 30 '20 at 14:33
  • 5
    It is so sad that such a poorly designed language has gained so much popularity... – pishpish Jan 05 '21 at 17:22
  • 5
    It is so sad that you didn't take the time to understand the design choices in the language. – Adam Arold Feb 10 '21 at 08:58
  • 2
    In my experiments, putting `declare global` produces a compile error -- "Augmentations for the global scope can only be directly nested in external modules or ambient module declarations.ts". Is this still possible to enhance String, without having to import something every time? – GGizmos Jan 09 '22 at 20:50
  • used this in 4.3.5 and now trying to use it in 4.6.2 and for some reason, there are issues. – petrosmm Apr 03 '22 at 22:27
  • @AdamArold, that's not a fair comment, at all. Similar "design choices" in other languages like extending basic type functionality are straight-forward, and just...work. Look up the meaning of a Hobsian choice. – toddmo Nov 30 '22 at 16:35
28

Here is a working example, a simple Camel Case string modifier.

in my index.d.ts for my project

interface String {
    toCamelCase(): string;
}

in my root .ts somewhere accessible

String.prototype.toCamelCase = function(): string { 
    return this.replace(/(?:^\w|[A-Z]|-|\b\w)/g, 
       (ltr, idx) => idx === 0
              ? ltr.toLowerCase()
              : ltr.toUpperCase()
    ).replace(/\s+|-/g, '');
};

That was all I needed to do to get it working in typescript ^2.0.10.

I use it like str.toCamelCase()

update

I realized I had this need too and this is what I had

interface String {
    leadingChars(chars: string|number, length: number): string;
}


String.prototype.leadingChars = function (chars: string|number, length: number): string  {
    return (chars.toString().repeat(length) + this).substr(-length);
};

so console.log('1214'.leadingChars('0', 10)); gets me 0000001214

Quantum
  • 1,456
  • 3
  • 26
  • 54
26

For me the following worked in an Angular 6 project using TypeScript 2.8.4.

In the typings.d.ts file add:

interface Number {
  padZero(length: number);
}

interface String {
  padZero(length: number);
}

Note: No need to 'declare global'.

In a new file called string.extensions.ts add the following:

interface Number {
  padZero(length: number);
}

interface String {
  padZero(length: number);
}

String.prototype.padZero = function (length: number) {
  var s: string = String(this);
  while (s.length < length) {
    s = '0' + s;
  }
  return s;
}

Number.prototype.padZero = function (length: number) {
  return String(this).padZero(length)
}

To use it, first import it:

import '../../string.extensions';

Obviously your import statement should point to the correct path.
Inside your class's constructor or any method:

var num: number = 7;
var str: string = "7";
console.log("padded number: ", num.padZero(5));
console.log("padding string: ", str.padZero(5));
Jacques
  • 6,936
  • 8
  • 43
  • 102
6

2022 typescript 4.9

The solution is pretty simple but getting to that solution was not. Ignore the top voted answer; most of that isn't needed and doesn't work any more.

index.d.ts

Not needed!

strings.ts

interface String {
    toProperCase(): string;
    bool(): boolean;
}

String.prototype.toProperCase = function (): string {
    return this.toLowerCase().replace(/\b\w/g, (c: string) => c.toUpperCase())
}

String.prototype.bool = function (): boolean {
    return this.toLowerCase() == 'true';
};

using it

import './lib/strings'

var a = 'hello'
var b = a.toProperCase()

outputs:

Hello

toddmo
  • 20,682
  • 14
  • 97
  • 107
  • 2
    If we have some import (let's say helper function for logging) on top of `interface String` in `strings.ts`. TypeScript gives the error `Property 'x' does not exist on type 'String' `. @toddmo do you have any clue how to resolve this? – JD Solanki Dec 01 '22 at 12:04
  • @JDSolanki, your interface statements need to come first in the file for the compiler. Import statements can go lower and don't need to be on top. They only need to be above where they are used. – toddmo Dec 01 '22 at 14:32
  • Worked fine for me. – bechir Feb 02 '23 at 01:36
  • @toddmo Unfortunately with `import` in between interface and String.prototype it simply doesn't work for me, always `property does not exist on type` – zaitsman Aug 08 '23 at 00:20
0

A slight modification on toddmo's answer: I needed to declare the interface via declare global {}:

declare global {
    interface String {
        repeatAction: (action: (str: string) => string) => string;
    }
}

String.prototype.repeatAction = function(this: string, action: (str: string) => string) {
    let str = this;
    do {
        var tempStr = str;
        str = action(str);
    } while (tempStr != str);

    return str;
};
Star Brood
  • 31
  • 4
  • What is the difference between your answer and [Steven's](https://stackoverflow.com/a/53392268/3997521)? – petrosmm Feb 14 '23 at 15:25