Reputation: 53
I have a JavaScript module that has a custom .d.ts
file. Lets call the module Foobar
:
foobar.js
function foobar (opts) {
this.register = plugin => {
plugin(this)
}
this.decorate = (prop, value) => {
this[prop] = value
}
return this
}
export default foobar
foobar.d.ts
export interface FoobarPlugin {
(inst: FoobarInst): void
}
export interface FoobarInst {
register(plugin: FoobarPlugin): void
decorate(prop: string, value: any): void
}
export default function foobar (): FoobarInst
I also have plugin:
fuzzbuzz.js
function fuzzbuzz (inst) {
inst.decorate('fuzzbuzz', true)
}
export default fuzzbuzz
fuzzbuzz.d.ts
import { FoobarInst } from '../foobar/foobar'
export default function fuzzbuzz (inst: FoobarInst): void
I load the plugin into my module:
index.ts
import foobar from './foobar/foobar'
import fuzzbuzz from './fuzzbuzz/fuzzbuzz'
const inst = foobar()
inst.register(fuzzbuzz)
inst.fuzzbuzz // -> true
What do I need to add to fuzzbuzz.d.ts in order to update the FoobarInst type definition?
I have tried variations of:
declare module foobar {
interface FoobarInst {
fuzzbuzz: boolean
}
}
My tsconfig.json
looks like:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
}
}
Thank you for any help you can provide!
Upvotes: 4
Views: 768
Reputation: 53
Thank you @ford04 for a great reply. With the links provided I was able to solve my specific goal.
The important detail with my scenario is both foobar
and fuzzbuzz
are authored in JavaScript, and have custom .d.ts
definition files. Additionally, the foobar
module does not use declare module
but instead an export default
on the singular function the module natively exports.
The solution code is:
| - node_modules \
| - foobar \
| - foobar.js
| - foobar.d.ts
| - package.json
| - fuzzbuzz \
| - fuzzbuzz.js
| - fuzzbuzz.d.ts
| - package.json
| - index.ts
| - package.json
foobar.js
function foobar (opts) {
this.register = plugin => {
plugin(this)
}
this.decorate = (prop, value) => {
this[prop] = value
}
return this
}
export default foobar
foobar.d.ts
export interface FoobarPlugin {
(inst: FoobarInst): void
}
export interface FoobarInst {
register(plugin: FoobarPlugin): void
decorate(prop: string, value: any): void
}
export default function foobar (): FoobarInst
foobar/package.json
{
"main": "foobar.js",
"types": "foobar.d.ts",
}
fuzzbuzz.js
function fuzzbuzz (inst) {
inst.decorate('fuzzbuzz', true)
}
export default fuzzbuzz
fuzzbuzz.d.ts
import { FoobarInst } from 'foobar'
declare module "foobar" {
interface FoobarInst {
fuzzbuzz: boolean
}
}
export default function fuzzbuzz (inst: FoobarInst): void
fuzzbuzz/package.json
{
"main": "fuzzbuzz.js",
"types": "fuzzbuzz.d.ts",
}
index.ts
import foobar from 'foobar'
import fuzzbuzz from 'fuzzbuzz'
const inst = foobar()
inst.register(fuzzbuzz)
inst.fuzzbuzz // -> true and no type error!
Important Details
Specify the type definitions in package.json
declare module '<library>'
will merge declarations even if the <library>
does not use declare module
explicitly (I believe it is still considered a module by TypeScript regardless).
This prototype augmentation / decoration is error prone in the sense that just by importing fuzzbuzz
into index.ts
will cause the types to merge...even if you do not call .register(fuzzbuzz)
. @ford04 answer provides more context on this Therefore, it makes sense to place the type augmentation and foobar.register(fuzzbuzz) in one module, so that types and run-time code are in sync.
Upvotes: 0
Reputation: 74500
What you are looking for is module augmentation. I think, an import
statement is missing in fuzzbuzz.d.ts
to let TS recognize, that the given declarations extend/augment an already existing "foobar"
module:
fuzzbuzz.d.ts:
import { FoobarOpts } from "foobar"; // can be any import, preferrably one of "foobar"
declare module "foobar" {
// your module extensions
}
The plugin extensions have to be statically analyzable by the compiler. That means, you cannot make TS augment the foobar
as soon as foobar.register(fuzzbuzz)
is called or via dynamic import.
Instead, the module is seen as augmented, when fuzzbuzz.d.ts
is included as input for the compilation either by module resolution or automatic inclusion of .ts
/.d.ts
files in the project directory. Therefore, it makes sense to place the type augmentation and foobar.register(fuzzbuzz)
in one module, so that types and run-time code are in sync. A minimal example:
foobar.ts:
declare module "foobar" {
// Plugin gets the options and possibly some internal "foobar" state
type Plugin = (opts: FoobarOpts, state: {}) => void;
interface FoobarOpts {
foo: string;
bar: number;
}
export default function foobar(opts: FoobarOpts): void;
function register(plugin: Plugin): void;
}
my-plugin.ts:
import { register, Plugin, FoobarOpts } from "foobar";
const fuzzBuzzPlugin: Plugin = (opt: FoobarOpts, state) => {
opt.fuzzbuzz; // fuzzbuzz available now.
};
// set type augmentation...
declare module "foobar" {
export interface FoobarOpts {
fuzzbuzz: boolean;
}
}
// ... and run-time plugin extension in one module, so they go hand in hand
register(fuzzBuzzPlugin);
client.ts:
import foobar from "foobar";
foobar({ bar: 42, foo: "buh", fuzzbuzz: true }); // works with fuzzbuzz
Upvotes: 3