43
votes

I have a declaration file in my TypeScript project like so:

// myapp.d.ts
declare namespace MyApp {
  interface MyThing {
    prop1: string
    prop2: number
  }
}

This works great and I can use this namespace anywhere in my project without having to import it.

I now need to import a type from a 3rd party module and use it in my ambient declaration:

// myapp.d.ts
import {SomeType} from 'module'

declare namespace MyApp {
  interface MyThing {
    prop1: string
    prop2: number
    prop3: SomeType
  }
}

The compiler now complains that it can't find namespace 'MyApp', presumably because the import prevents it from being ambient.

Is there some easy way to retain the ambient-ness of the declaration whilst utilising 3rd-party types?

4
@RomanStarkov This is not possible for any arbitrary library, do you have a specific one in mind ?Titian Cernicova-Dragomir
@TitianCernicova-Dragomir Let's say luxon, it comes with typings and looks just like this sample code here - import { DateTime } from 'luxon'.Roman Starkov

4 Answers

53
votes

Yes, there is a way. It has become easier in TypeScript 2.9 by using import() a type but it's also possible in earlier versions of TypeScript.

The following file is a script. Things declared in a script are added to the global scope. This is why you can use MyApp.MyThing without importing it.

// myapp.d.ts
declare namespace MyApp {
  interface MyThing {
    prop1: string;
    prop2: number;
  }
}

Scripts are limited in that they cannot import anything; when you add an import, the script becomes a module. I think it's weird to say the least, but it is what it is. What matters is that things defined in a module are scoped to that module, and are not added to the global scope.

However, modules can add declarations to the global scope too, by putting them inside global:

// myapp.d.ts
import {SomeType} from 'module';

declare global {
  namespace MyApp {
    interface MyThing {
      prop1: string;
      prop2: number;
      prop3: SomeType;
    }
  }
}

This file is a module, but it adds a declaration of MyApp.MyThing to the global scope, so you can still use MyApp.MyThing in other TypeScript code without importing it.

Note that using the .d.ts extension has nothing to do with you being able to access the interface without importing it. Both of the above files could have been .ts files and would still behave exactly the same.

21
votes

Since TS 2.9 this is possible with import():

// myapp.d.ts
declare type SomeType = import('module').SomeType;
declare type SomeDefaultType = import('module-with-default-export').default;


declare namespace MyApp {
  interface MyThing {
    prop1: string;
    prop2: number;
    prop3: SomeType | SomeDefaultType;
  }
}
1
votes

Unfortunately, no. As you already figured out this only works with internal code, e.g. without external dependencies. You should either go with exporting your namespace, or go for export of classes and use ES6 modules. But both will result in you requiring to import your stuff. Something that you're trying to avoid, as I believe.

Personally, I find it more comforting to actually use imports (even internals) throughout the code. This for the simple reason that when opening a specific file (class), all its dependencies are immediately visible.

A thorough example was already addressed in the question "How to use namespaces with import in TypeScript".

Note for others: the "namespace being available internally" is also the reason why I'm not considering this a duplicate question.

1
votes

Don't know if you're still looking for an answer, but this is the correct way to handle it and still be able to e.g. define generic modules, not just named namespaces: (original answer this is based on)

// myapp.d.ts

declare namespace MyApp {
  import {SomeType} from 'module'
  interface MyThing {
    prop1: string
    prop2: number
    prop3: SomeType
  }
}