Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript module augmentation with modules in multiple files

Tags:

typescript

I've successfully used typescript "module augmentation" in the past as a temporary workaround when the type declarations from DefinitelyTyped lagged behind the underlying javascript library. The type definitions were always contained in a single file, however, and a new version of a particular public library I'm using has broken the definitions into multiple modules with re-exporting, etc.

In the past this would have worked, but now it doesn't:

import * as Sequelize from 'sequelize';
declare module 'sequelize' {
  interface HasManyOptions {
    sourceKey?: string;
  }

So how can I add add sourceKey to the HasManyOptions interface with the file structure below? I've tried messing with the name of the imported module and nesting module declarations, but no love. I'm stumped.


index.d.ts

export * from './lib/sequelize'

./lib/sequelize.d.ts

export * from './associations/index'

./lib/associations/index.d.ts

export * from './has-many'

./lib/associations/has-many.d.ts

export interface HasManyOptions extends ManyToManyOptions {
    keyType?: DataType
    ...
}
like image 524
BillyB Avatar asked Nov 18 '18 22:11

BillyB


2 Answers

Thanks for the repro case @BillyB. You want this:

import { ManyToManyOptions, DataType } from 'sequelize'

declare module 'sequelize' {
  interface HasManyOptions extends ManyToManyOptions {
    /**
     * A string or a data type to represent the identifier in the table
     */
    keyType?: DataType
    sourceKey?: string
  }
}

Module augmentations aren't super well documented. Here's how this works:

  1. You declare module 'sequelize', telling TS that you're declaring the sequelize module. The way name merging works in TS, TS will merge all modules with the exact path 'sequelize' into one module.
  2. The way interface merging works in TS, your interfaces have to not only have the same name, but their entire declarations has to be identical (this includes extends clauses and type parameters). So we have to copy keyType over from the source declaration, which is a little gross.

I would file a bug in TypeScript's issue tracker for this. It's surprising that TSC didn't error when you tried to declare an interface with the same exact name as an existing name as an augmentation (shadowing the original name). TSC should either throw an exception that your module exports two unrelated interfaces with the same name, or TS should not take extends clauses into account when performing interface merging (at least in the specific case of module augmentations).

like image 134
bcherny Avatar answered Oct 30 '22 14:10

bcherny


This is working on my machine:

sequelize.d.ts

import 'sequelize';

declare module 'sequelize' {
    interface ManyToManyOptions {
        sourceKey?: string;
    }
}

index.ts

import { ManyToManyOptions, AssociationOptions } from 'sequelize';

const options: AssociationOptions = {};

const optionsToo: ManyToManyOptions = {
    sourceKey: 'foo',
};

https://github.com/shaunluttin/typescript-module-augmentation-sequelize

Edit: This is the fix that worked for BillyB.

To recap for future readers, a 2 part solution to my problem: In these multi-file situations with re-exported declarations the module augmention needed to be in a separate file for some reason; this was not the case in a single typings file. Second, if you are augmenting interfaces that extend other interfaces, you must repeat the extends portion of the declaration; again this was not necessary with a single typing file.

like image 38
Shaun Luttin Avatar answered Oct 30 '22 14:10

Shaun Luttin