How do you produce a .d.ts "typings" definition file from an existing JavaScript library?
Asked Answered
H

10

235

I'm using a lot of libraries both my own and 3rd party. I see the "typings" directory contains some for Jquery and WinRT... but how are they created?

Hamburger answered 2/10, 2012 at 9:29 Comment(0)
O
277

There are a few options available for you depending on the library in question, how it's written, and what level of accuracy you're looking for. Let's review the options, in roughly descending order of desirability.

Maybe It Exists Already

Always check DefinitelyTyped (https://github.com/DefinitelyTyped/DefinitelyTyped) first. This is a community repo full of literally thousands of .d.ts files and it's very likely the thing you're using is already there. You should also check TypeSearch (https://microsoft.github.io/TypeSearch/) which is a search engine for NPM-published .d.ts files; this will have slightly more definitions than DefinitelyTyped. A few modules are also shipping their own definitions as part of their NPM distribution, so also see if that's the case before trying to write your own.

Maybe You Don't Need One

TypeScript now supports the --allowJs flag and will make more JS-based inferences in .js files. You can try including the .js file in your compilation along with the --allowJs setting to see if this gives you good enough type information. TypeScript will recognize things like ES5-style classes and JSDoc comments in these files, but may get tripped up if the library initializes itself in a weird way.

Get Started With --allowJs

If --allowJs gave you decent results and you want to write a better definition file yourself, you can combine --allowJs with --declaration to see TypeScript's "best guess" at the types of the library. This will give you a decent starting point, and may be as good as a hand-authored file if the JSDoc comments are well-written and the compiler was able to find them.

Get Started with dts-gen

If --allowJs didn't work, you might want to use dts-gen (https://github.com/Microsoft/dts-gen) to get a starting point. This tool uses the runtime shape of the object to accurately enumerate all available properties. On the plus side this tends to be very accurate, but the tool does not yet support scraping the JSDoc comments to populate additional types. You run this like so:

npm install -g dts-gen
dts-gen -m <your-module>

This will generate your-module.d.ts in the current folder.

Hit the Snooze Button

If you just want to do it all later and go without types for a while, in TypeScript 2.0 you can now write

declare module "foo";

which will let you import the "foo" module with type any. If you have a global you want to deal with later, just write

declare const foo: any;

which will give you a foo variable.

Oldenburg answered 2/10, 2012 at 17:11 Comment(10)
Ouch... that's going to be a significant barrier for entry in many cases. It would be nice to have a tool that could output at least "best guesses" based on type inference. While these might not be optimal, they could at least be tuned over time.Hamburger
+1 - Further good news: --declarations generates both the .js file and the .d.ts file, which means you only need to run a single compile.Cassiani
A collection of high-quality definitions for popular libraries can be found at github.com/borisyankov/DefinitelyTypedJudicative
A 'best guess' tool will not be any better than the IntelliSense already existing in TypeScript. The benefit of the type definitions is in providing more information to the compiler than it can 'guess'.Judicative
@BorisYankov the file would only serve as a starting point, bag-of-ideas kind of thing, which the user will proceed to modify later...Faldstool
@GorgiKosev I see your point. On the other hand, the idea of sitting down with the documentation and therefore basing the declaration file on "how it should be" and "what the author meant" instead of "how it is" has much to speak for it. When computers infer typing, they try to be as inclusive as possible. "This function can work perfectly well for both strings and numbers, let's allow both". People tend to be restrictive. "My nice workOnStrings function takes a string and that's that". The computer approach is good for static analysis. The human approach is good for communicating intent.Piecework
Note that github.com/DefinitelyTyped/DefinitelyTyped on github is a repository for a vast collection of type definitions for popular libraries.Sauder
If you have documented the library via JSDoc then I cannot see why an automated script couldn't use that data to generate typescript definition files. Making this stuff by hand for a large library is so much work for such a small percentage of users it makes it kinda pointless.Grot
--allowJs with --declaration options can't be combined (tested in TypeScript 1.8 and 2.0). If I try, I get: error TS5053: Option 'allowJs' cannot be specified with option 'declaration'Olander
I'm reading the documentation about how to type what is in my case a very simple javascript file and it is ridiculously complex. I would just give up immediately.Surefire
C
71

You can either use tsc --declaration fileName.ts like Ryan describes, or you can specify declaration: true under compilerOptionsin your tsconfig.json assuming you've already had a tsconfig.json under your project.

Chrono answered 4/8, 2016 at 0:54 Comment(4)
This is the best answer if you are developing an Angular 2 module(s) you'd like to share.Luciennelucier
If I try tsc --declaration test.ts I get the error Cannot find name... for the types that I'm trying to create a declaration file for :) So I need the types before I can declare them?Tearful
@Kokodoko, can you try add declaration: true to your tsconfig.json file?Chrono
Thanks, I just was wanting to generate *.d.ts from *.ts.Prevention
I
26

When creating your own library, you can can create *.d.ts files by using the tsc (TypeScript Compiler) command like so: (assuming you're building your library to the dist/lib folder)

tsc -d --declarationDir dist/lib --declarationMap --emitDeclarationOnly
  • -d (--declaration): generates the *.d.ts files
  • --declarationDir dist/lib: Output directory for generated declaration files.
  • --declarationMap: Generates a sourcemap for each corresponding ‘.d.ts’ file.
  • --emitDeclarationOnly: Only emit ‘.d.ts’ declaration files. (no compiled JS)

(see the docs for all command line compiler options)

Or for instance in your package.json:

"scripts": {
    "build:types": "tsc -d --declarationDir dist/lib --declarationMap --emitDeclarationOnly",
}

and then run: yarn build:types (or npm run build:types)

Izy answered 23/9, 2019 at 16:24 Comment(6)
I still cannot figure out how to generate the d.ts files and be able to use interfaces. Do you have any examples?Dinsmore
The npm script above will generate the *.d.ts files and put them in the dist/lib folder. You do need a tsconfig.json file in the root of your project, but that should be there for the project to work anyway.Izy
I am unable to use them, once exposed. I have the following post stackoverflow.com/questions/59742688 if you can make it work?Dinsmore
Thanks magikMaker. This should be the accepted answer!Exanthema
@Izy as I understand tsconfig.json allows you to define those options in a file, so you could have { "include": ["src/**/*"], "compilerOptions": { "allowJs": true, "declaration": true, "emitDeclarationOnly": true, "outDir": "dist/lib", "declarationMap": true }} and running tsc on its own would do the same as you command line options.Recapture
Here's the TypeScript docs for declaration files with TSConfigRecapture
P
18

As Ryan says, the tsc compiler has a switch --declaration which generates a .d.ts file from a .ts file. Also note that (barring bugs) TypeScript is supposed to be able to compile Javascript, so you can pass existing javascript code to the tsc compiler.

Plage answered 2/10, 2012 at 23:2 Comment(2)
seems currently passing *.js files into tsc generates an error "Error reading file "angular-resource.js": File not found". So you have to explicitly rename *.js files to *.ts to be able to use tsc. See this issue for more details - I tried using this on angularjs: typescript.codeplex.com/workitem/26Shwa
from what I can tell, tsc can compile any valid JavaScript, but if it is not valid TypeScript (compile gives warnings) then the --declarations flag does not output a .d.ts file, so unless your library is written in TypeScript you still need to manually create the declarations file.Sohn
F
18

The best way to deal with this (if a declaration file is not available on DefinitelyTyped) is to write declarations only for the things you use rather than the entire library. This reduces the work a lot - and additionally the compiler is there to help out by complaining about missing methods.

Faldstool answered 19/10, 2015 at 9:8 Comment(1)
The documentation unfortunately is hopeless.Surefire
L
7

as described in http://channel9.msdn.com/posts/Anders-Hejlsberg-Steve-Lucco-and-Luke-Hoban-Inside-TypeScript at 00:33:52 they had built a tool to convert WebIDL and WinRT metadata into TypeScript d.ts

Lewendal answered 2/10, 2012 at 20:24 Comment(0)
B
4

To make things extremely simple for TypeScript newbies like me:

tsc --declaration FileName.js --allowJs

This command will generate a d.ts file in your current folder. You need to have TypeScript installed, but you don't need to be the project maintainer, and you don't need to have a larger compilation process set up. You can hand it any old JavaScript file and it will attempt to generate type definitions for you.

Banwell answered 29/3, 2023 at 15:1 Comment(0)
L
3

Here is some PowerShell that creates a single TypeScript definition file a library that includes multiple *.js files with modern JavaScript.

First, change all the extensions to .ts.

Get-ChildItem | foreach { Rename-Item $_ $_.Name.Replace(".js", ".ts") }

Second, use the TypeScript compiler to generate definition files. There will be a bunch of compiler errors, but we can ignore those.

Get-ChildItem | foreach { tsc $_.Name  }

Finally, combine all the *.d.ts files into one index.d.ts, removing the import statements and removing the default from each export statement.

Remove-Item index.d.ts; 

Get-ChildItem -Path *.d.ts -Exclude "Index.d.ts" | `
  foreach { Get-Content $_ } | `
  where { !$_.ToString().StartsWith("import") } | `
  foreach { $_.Replace("export default", "export") } | `
  foreach { Add-Content index.d.ts $_ }

This ends with a single, usable index.d.ts file that includes many of the definitions.

Lenka answered 19/8, 2016 at 22:44 Comment(1)
Second step, Get-ChildItem | foreach { tsc --declaration $_.Name } worked (added missing --declaration param)Kerril
K
2

I would look for an existing mapping of your 3rd party JS libraries that support Script# or SharpKit. Users of these C# to .js cross compilers will have faced the problem you now face and might have published an open source program to scan your 3rd party lib and convert into skeleton C# classes. If so hack the scanner program to generate TypeScript in place of C#.

Failing that, translating a C# public interface for your 3rd party lib into TypeScript definitions might be simpler than doing the same by reading the source JavaScript.

My special interest is Sencha's ExtJS RIA framework and I know there have been projects published to generate a C# interpretation for Script# or SharpKit

Katanga answered 4/10, 2012 at 17:37 Comment(2)
FYI: I just made my first typescript definition file for Sencha/ExtJS: github.com/andremussche/extjsTypescript/blob/master/jsondocs/…Gravely
hey Camel Case, I have put together a blog on using ExtJs with typescript: blorkfish.wordpress.com/2013/01/28/using-extjs-with-typescriptEfficiency
P
2

This answer shows how to do it in a stand alone function using the typescript compiler API:

import ts from 'typescript';
function createDTsTextsFromJsFilePaths(
  jsFileNames: string[]
): Record<string, string> {
  const options = {
    allowJs: true,
    declaration: true,
    emitDeclarationOnly: true,
  };
  // Create a Program with an in-memory emit
  const createdFiles: Record<string, string> = {};
  const host = ts.createCompilerHost(options);
  host.writeFile = (fileName: string, contents: string): void => {
    const dtsName = fileName.replace('.js', '.d.ts');
    createdFiles[dtsName] = contents;
  };
  // Prepare and emit the d.ts files
  const program = ts.createProgram(jsFileNames, options, host);
  program.emit();
  return createdFiles;
}
////////////// test code below here ///////////////////////////////////
import fs from 'fs'
const jsTest = 
`
"use strict"
function x(){
  return {
    a: 1, 
    b: ()=>'hello'
  }
}
`;
const jsPath = '/tmp/jstest.js';
fs.writeFileSync(jsPath, jsTest);
const createdFiles = createDTsTextsFromJsFilePaths([jsPath]);
for (const dtsName in createdFiles) {
  console.log('================');
  console.log(dtsName);
  console.log('----------------');
  console.log(createdFiles[dtsName]);
}

Execution of the test gives the result

================
/tmp/jstest.d.ts
----------------
declare function x(): {
    a: number;
    b: () => string;
};

The function is derived from documentation in the type script compiler API guide

Predikant answered 5/8, 2021 at 23:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.