#Module Augmentation on Type Alias

16 messages · Page 1 of 1 (latest)

halcyon ruin
#

Hi,
I want to modify a type alias via module augmentation:

// SomeModule/index.d.ts
export type Bar = { baz: 42 }
// my module
import 'Foo'

declare module 'Foo' {
  export type Bar = { qux: 42 }  // ❌ duplicate identifier "Bar", also declared in SomeModule/index.d.ts
}

this will cause TS to complain about Bar being a duplicate identifier. I found one workaround that refers to a GH issue from 2018 and assumes you can modify the original type definition and work in interfaces instead of type aliases. Is there another approach that does not require me to modify the original module?

glass mulch
#

No, types are meant to be not modifiable.

graceful birch
#

If you put your declare in a non-module d.ts file it will override the types of that module entirely

#

In a module it just combines them.

#

I guess a non-module .ts file would too, but we don't have this in node, so I'm not sure.

halcyon ruin
glass mulch
#

Imo it is, and being no modifiable is one of the reasons.

#

Interface is like the var of TS.

halcyon ruin
#

I get the sentiment. But it directly opposes the feature of module augmentation. (I never was a big fan of final and similar concepts in Java et al.)

glass mulch
#

I've answered a few of your questions and I could be remembering wrong, but I think you work with a very specific type of projects that may have skewed your view. Module augmentation/declaration merging is not something you want to actively use, it's more of a last resort thing.

#

Let's take a simple example, an awesome-point library project perhaps have some code like:

export interface Point {
    x: number
    y: number
}

export function mirror(p: Point): Point {
    return {
        x: -p.x,
        y: -p.y,
    }
}

It all seems fine on purpose, but if the consumer project was to augment Point:

import { mirror } from 'awesome-point'

declare module 'awesome-point' {
    interface Point {
        z: number
    }
}

const result = mirror({
    x: 42,
    y: 69,
    z: 1337,
})

// Oh no, explode at runtime
console.log(result.z.toString())
#

The moment you wrote interface Point, your code is more or less doomed because interface is open to augmentation and you lose practically all guarantees, without compiler helping you.
In order to correctly write code WRT that, you would have to either write:

- export function mirror(p: Point): Point {
+ export function mirror(p: Point) {
    return {
        x: -p.x,
        y: -p.y,
    }
}

Or to write:

export function mirror(p: Point): Point {
    return {
+       ...p,
        x: -p.x,
        y: -p.y,
    }
}

Both solutions suck in their own ways, and to someone reading that piece of code that is not familiar with interface's openness, that code might even seem weird/written incorrectly.

#

I don't have any number to back this up, but I would not be surprised if you declaration merge into libraries, vast vast majority of them will break.

glass mulch
halcyon ruin
#

I fully get your point and it at the same time demonstrates the scenario where the type given by the author could be more restrictive than the implementation actually is.
For my concrete use case, the exported type looks something like this:

export class Thing { … }
export type ThingRegistry = { [key: string]: Thing }

and I was asked to retroactively stricten the type so that the keys are suggested. This would be possible, because on the consumer side all possible Thing subclasses are known. I see and agree that from an academic, purist point of view it makes sense to use module augmentation sparingly at best. But for my real life use case that I am forced to work with (yes, you do remember correctly, and I am not thrilled either), I just have to somehow make it work.

halcyon ruin
#

Anyway, can't be helped. Thanks for the replies!