How to extend an interface declared in an external library d.ts?
Asked Answered
D

7

42

I installed the knockout definitions using the documented method like this.

npm install @types/knockout

It works nicely, I could import it like this anywhere.

import * as ko from "knockout";

However, I'm stuck with extending KnockoutStatic interface with some custom stuff. I'm trying to migrate a <reference ... /> and namespace based huge TS application to use modules. Before, I easily declared the extension interface anywhere and the declarations got merged. Let's say my extension looks like this.

interface KnockoutStatic {
  doSomething(): void;
}

I tried to create a KnockoutExtensions.d.ts file where I declared it like this.

import "knockout";

declare module "knockout" {
  export interface KnockoutStatic {
    doSomething(): void;
  }
}

But when I import both knockout and my extension somewhere, TS still cannot resolve the doSomething calls.

import * as ko from "knockout";
import "./KnockoutExtensions";

ko.doSomething(); // error

What is the proper method of extending library interfaces using TypeScript 2.0 and the new d.ts subsystem?

I'm using Visual Studio 2015 Update 3 with TypeScript 2.0 installed.

Delanadelancey answered 4/10, 2016 at 13:37 Comment(0)
S
25

You can easily extend the 'knockout' or any other TypeScript namespace.

Example: create knockout-extension.d.ts file

/// <reference path="<path-to-typings-dir>/knockout/index.d.ts" />

declare module 'knockout' {

  export interface CustomType {

    customField: string;

    customMethod(arg1: number, arg2: boolean): boolean;
  }

  namespace customNamespace {

    export interface AnotherCustomType {
      customField1: string;
      customField2: boolean;
    }
  }

  // NOTE: extending existing interface
  export interface KnockoutStatic {
    customMethod(): void;
  }
}

Note: ensure that this file is picked-up by the TypeScript compiler.

Use the newly defined types from the extended module.

// one way
import { CustomType } from 'knockout';

const foo: CustomType;

// second way
import * as kc from 'knockout';

const foo: kc.CustomType;
const bar: kc.customNamespace.AnotherCustomType;

For more info on modules and namespaces you can check TypeScript documentation on Modules and Namespaces and using them together.

Cheers!

Steamtight answered 29/6, 2017 at 15:8 Comment(0)
C
19

I found that winston has the same problem, using the export = syntax. I found this page helpful when it showed that react did the same thing: https://www.credera.com/blog/technology-solutions/typescript-adding-custom-type-definitions-for-existing-libraries/.

The solution they recommended, which I found worked is this:

import 'react';

declare module 'react' {
    interface OlHTMLAttributes<T> {
        type?: "1" | "a" | "A" | "i" | "I";
    }
}

Simply importing the module, and then declaring it allows any interfaces in this declaration block to extend the existing ones and in other parts of you code you can go on using the interface as you normally would; i.e. you would still import react or winston or knockout and you'd see these new interface members. You don't have to start referencing a custom interface or anything like that.

Clouet answered 2/10, 2019 at 14:9 Comment(0)
D
3

The problem is that knockout typing file uses the export = syntax and it's not "augmentation friendly". See this as a reference.

The simplest solution for me was to wrap the extensions in declare global { } as knockout typing file declares everything in global scope.

declare global {
  interface KnockoutStatic {
    doSomething(): void;
  }
}
Delanadelancey answered 4/10, 2016 at 14:40 Comment(0)
C
2

I was trying to extend the Express Request object so that I can use req.log.info to log something. The following settings works for me

// tsconfig.json
{
  "compilerOptions": {
    ...,
    "baseUrl": ".",
    "paths": {
      "*": ["node_modules/*", "types/*"]
    }
  },
  "include": ["src/**/*"],
  "files": ["./types/express-extension.d.ts"],
  "ts-node": {
    "files": true
  }
}

// types/express-extension.d.ts
import 'express';

declare module 'express' {
  export interface Request {
    log?: {
      debug: (str: string) => void,
      info: (str: string) => void,
      warn: (str: string) => void,
      error: (str: string) => void,
    }
  }
}

My file structure looks like this:

src
|_ ...
types
|_ express-extension.d.ts
tsconfig.json
package.json
Cyte answered 22/11, 2021 at 4:0 Comment(0)
C
1

This code works for me

// observable.ts
export class Observable<T> {
  // ... implementation left as an exercise for the reader ...
}

// map.ts
import { Observable } from "./observable";
declare module "./observable" {
  interface Observable<T> {
    map<U>(f: (x: T) => U): Observable<U>;
  }
}
Observable.prototype.map = function (f) {
  // ... another exercise for the reader
};

// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map((x) => x.toFixed());
Comestible answered 21/1, 2021 at 22:7 Comment(0)
E
0

The task was to add additional colors for Dark and Light modes for a Material UI theme in a React app. Therefore, I needed to add an additional property to Palette interface of Material UI module @mui/material. It turned out that the problem can be solved with Module Augmentation.

  1. First you need to add a typeRoots to your tsconfig.json where you would define your custom types. I typically use ./src/types folder.
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    },
    "typeRoots": ["./node_modules/@types", "./src/types"]
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
  1. You need to create a ts file in this folder. You can use any name you like. I named file mui_material.ts. You need to import your module and to declare it. Here I augmented Palette interface and added section type.
import '@mui/material';

declare module '@mui/material' {
  export interface Palette {
    section: {
      mainColor: string;
      sectionBackgroundColor: string;
      buttonColor: string;
      buttonTxtColor: string;
      textColor: string;
      spotlightCardColor: string;
      flowFeedTitleTextColor: string;
      newsCardColor: string;
      newsTextColor: string;
      newsBorderColor: string;
      timeTextColor: string;
      timeColor: string;
      scrollColor: string;
      scrollBgColor: string;
    };
  }
}
  1. Now you can do this theme.palette.section and to have intellisense, because palette has section property. This is MyComponent.tsx
import * as React from 'react';
import Card from '@mui/material/Card';
import { Box, useTheme } from '@mui/material';

function MyComponent() {
  const theme = useTheme();

  return (
    <Card
      sx={{ backgroundColor: theme.palette.section.spotlightCardColor }}
    >);
}
Escolar answered 28/5 at 1:54 Comment(0)
M
-2

You need to create your interface outside of your module. Do not declare it with export.

module example  {
   //...do stuff
}
 
interface KnockoutStatic {
  doSomething(): void;
}

You can add it in a separate file like where you add your interface extensions to keep it clean.

Microgamete answered 4/10, 2016 at 14:33 Comment(1)
This didn't work, this worked only when I didn't use modules, but pure namespaces.Epilate

© 2022 - 2024 — McMap. All rights reserved.