6

I Kotlin if I have an interface like this:

interface User {
    val name: String
    val email: String
}

I can write an extension function like this anywhere in the code:

fun User.toUserDto(): UserDto {
    TODO()
}

In Typescript if I have a similar interface:

export default interface User {
    name: string;
    email: string;
}

How can I augment it in a similar way? Is this a best practice in the language? Is there an alternative to this I don't know about?

Aluan Haddad
  • 29,886
  • 8
  • 72
  • 84
Adam Arold
  • 29,285
  • 22
  • 112
  • 207

3 Answers3

2

You can augment classes like this, but it depends on the prototype property and there's no User.prototype (and no User value at all either).

You can also see this very long discussion. The explanation why the Kotlin/C# approach was rejected is here.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • What is the best practice when I want to augment classes I have no control over in the Typescript way? – Adam Arold Feb 11 '21 at 00:40
  • For classes, see the first link or another example here https://stackoverflow.com/posts/38434418, but it isn't nearly as common as in Kotlin. I am not experienced enough in TypeScript to judge about best practice, but it seems to be fine. – Alexey Romanov Feb 11 '21 at 07:35
  • One difference that may or may not matter for your usecase is that augmentations are actually added to the prototype object, so e.g. Javascript clients will see them, and two "extension functions" with the same name on the same class (or subclass) will break. – Alexey Romanov Feb 11 '21 at 07:45
0

Interfaces in Typescript can only describe the shape of data, you cannot make instances of them or mutate them.

To write an extension function of an interface, you would need to implement the interface in a class, and add whatever you need on the class.

export class SomeUserClass implements User {
  name: string;
  email: string;
  
  constructor(name, email) {
    ...
  }

  toUserDto() {
    ...
  }
}
inorganik
  • 24,255
  • 17
  • 90
  • 114
  • 2
    This is not an answer to my question. Extension functions are powerful because using them I can augment classes/interfaces **I have no control over** (like classes from dependencies). This is my use case. – Adam Arold Feb 09 '21 at 20:43
  • You can add a function to an interface, but not in the sense that a Kotlin extension does. In other words, you can add it to the type but not provide an implementation. – Aluan Haddad Feb 10 '21 at 20:03
  • @AluanHaddad exactly, it describes types – inorganik Feb 10 '21 at 21:05
  • I don't think you understand what I was getting at. Also, you make the false statement than interface cannot include functions. Interfaces describe the shapes of objects. Also, and JavaScript functions are data after the whole statement doesn't make any sense – Aluan Haddad Feb 10 '21 at 22:00
  • You didn't see that I edited my answer... should be pretty clear. – inorganik Feb 10 '21 at 22:33
  • 1
    I downvoted because the answer contains the line `Interfaces in Typescript are meant to describe the shape of data, they cannot include functions;` fix that – Aluan Haddad Feb 11 '21 at 03:34
  • 1
    No, it isn't. Please read https://www.typescriptlang.org/docs/handbook/interfaces.html and see "You can also describe methods in an interface that are implemented in the class" – Alexey Romanov Feb 12 '21 at 19:59
  • @AlexeyRomanov my goal with this answer is not to regurgitate the docs... I can see at least 3 ways to have function/method types described in an interface. The point of the answer is showing they aren't _callable_ in an interface. My answer already links to the docs, I think this is sufficient. – inorganik Feb 12 '21 at 20:28
  • 1
    But they are. Why do you think they aren't? – Alexey Romanov Feb 12 '21 at 20:29
  • If you have `export interface Foo { update: () => void }` you cannot call Foo.update() you can only implement a separate class that implements Foo and call update on the class. That's exactly what my answer explains – inorganik Feb 12 '21 at 20:36
  • 1
    You cannot call `Foo.update()` because `Foo` is not a value; you can call `foo.update()` on any `foo` of type `Foo`. – Alexey Romanov Feb 12 '21 at 20:39
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/228651/discussion-between-inorganik-and-alexey-romanov). – inorganik Feb 12 '21 at 20:42
  • Also, the type system is structural. Any object, be it an instance of a class or not implements an interface if it provides that interface's members. Even the implements clause is a mere formality as removing it does not change whether the class implements interface. – Aluan Haddad Feb 13 '21 at 23:56
0

Assuming your User interface is coming from a third party or is automatically generated, you have to to do 4 things:

  1. Define your desired interface

    export interface UserExtensions {
       toUserDto(): UserDTO
    }
    
  2. Tell the compiler that the generated/original interface provides the function (merge interfaces)

    declare module "./generated" { 
        interface User extends UserExtensions {}
    }
    
  3. Tell the compiler that underlying class provides the function (merge interface with class)

    declare module "./UserClassImpl" { 
        interface UserClass extends UserExtensions {}
    }
    
  4. Augment/Extend the class

    UserClass.prototype.toUserDto = (): UserDTO {
        return createUserDto(this)
    }
    

If there is no underlying class, you are out of luck. In case your data comes through axios, you could exchange the underlying anonymous json object with the true class through transformResponse.

HeikoG
  • 861
  • 10
  • 11