Skip to main content

2 posts tagged with "amd"

View All Tags

TypeScript Definitions, webpack and Module Types

A funny thing happened on the way to the registry the other day. Something changed in an npm package I was using and confusion arose. You can read my unfiltered confusion here but here's the slightly clearer explanation.

The TL;DR#

When modules are imported, your loader will decide which module format it wants to use. CommonJS / AMD etc. The loader decides. It's important that the export is of the same "shape" regardless of the module format. For 2 reasons:

  1. You want to be able to reliably use the module regardless of the choice that your loader has made for which export to use.
  2. Because when it comes to writing type definition files for modules, there is support for a single external definition. Not one for each module format.

The DR#

Once upon a time we decided to use big.js in our project. It's popular and my old friend Steve Ognibene apparently originally wrote the type definitions which can be found here. Then the definitions were updated by Miika Hänninen. And then there was pain.

UMD / CommonJS **and** Global exports oh my!#

My usage code was as simple as this:

import * as BigJs from 'big.js';
const lookABigJs = new BigJs(1);

If you execute it in a browser it works. It makes me a Big. However the TypeScript compiler is **not** happy. No siree. Nope. It's bellowing at me:

[ts] Cannot use 'new' with an expression whose type lacks a call or construct signature.

So I think: "Huh! I guess Miika just missed something off when he updated the definition files. No bother. I'll fix it." I take a look at how big.js exposes itself to the outside world. At the time, thusly:

//AMD.
if (typeof define === 'function' && define.amd) {
define(function () {
return Big;
});
// Node and other CommonJS-like environments that support module.exports.
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = Big;
module.exports.Big = Big;
//Browser.
} else {
global.Big = Big;
}

Now, we were using webpack as our script bundler / loader. webpack is supersmart; it can take all kinds of module formats. So although it's more famous for supporting CommonJS, it can roll with AMD. That's exactly what's happening here. When webpack encounters the above code, it goes with the AMD export. So at runtime, import * as BigJs from 'big.js'; lands up resolving to the return Big; above.

Now this turns out to be super-relevant. I took a look at the relevant portion of the definition file and found this:

export const Big: BigConstructor;

Which tells me that Big is being exported as a subproperty of the module. That makes sense; that lines up with the module.exports.Big = Big; statement in the the big.js source code. There's a "gotcha" coming; can you guess what it is?

The problem is that our type definition is not exposing Big as a default export. So even though it's there; TypeScript won't let us use it. What's killing us further is that webpack is loading the AMD export which doesn't have Big as a subproperty of the module. It only has it as a default.

Kitson Kelly expressed the problem well when he said:

there is a different shape depending on which loader is being used and I am not sure that makes a huge amount of sense. The AMD shape is different than the CommonJS shape. While that is technically possible, that feels like that is an issue.

One Definition to Rule Them All#

He's right; it is an issue. From a TypeScript perspective there is no way to write a definition file that allows for different module "shapes" depending upon the module type. If you really wanted to do that you're reduced to writing multiple definition files. That's blind alley anyway; what you want is a module to expose itself with the same "shape" regardless of the module type. What you want is this:

AMD === CommonJS === Global

And that's what we now have! Thanks to Michael McLaughlin, author of big.js, version 4.0 unified the export shape of the package. Miika Hänninen submitted another PR which fixed up the type definitions. And once again the world is a beautiful place!

TypeScript and RequireJS (Keep It Simple)

I'm not the first to take a look at mixing TypeScript and RequireJS but I wanted to get it clear in my head. Also, I've always felt the best way to learn is to do. So here we go. I'm going to create a TypeScript and RequireJS demo based on John Papa's "Keep It Simple RequireJS Demo".

So let's fire up Visual Studio 2013 and create a new ASP.NET Web Application called “RequireJSandTypeScript” (the empty project template is fine).

Add a new HTML file to the root called “index.html” and base it on “index3.html” from John Papa’s demo:

<!DOCTYPE html>
<html>
<head>
<title>TypeScript with RequireJS</title>
</head>
<body>
<div>
<h1>TypeScript with RequireJS loading jQuery in Visual Studio land</h1>
</div>
<!-- use jquery to load this message-->
<p id="message"></p>
<!-- Shortcut to load require and then load main-->
<script src="/scripts/require.js"
data-main="/scripts/main"
type="text/javascript"></script>
</body>
</html>

John’s demo depends on jQuery and RequireJS (not too surprisingly) so let’s fire up Nuget and get them:

Install-Package RequireJS
Install-Package jQuery

Whilst we’re at it, let’s get the Definitely Typed typings as well:

Install-Package jQuery.TypeScript.DefinitelyTyped

To my surprise this popped up the following dialog:

By "Your project has been configured to support TypeScript." it means that the csproj file has had the following entries added:

<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.Default.props" Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.Default.props')" />
...
<PropertyGroup>
...
<TypeScriptToolsVersion>0.9</TypeScriptToolsVersion>
</PropertyGroup>
...
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets" Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets')" />
...
</Project>

I’m not sure when this tweak to the Visual Studio tooling was added was added. Perhaps it's part of the TypeScript 1.0 RC release; either way it’s pretty nice. Let's press on.

Whilst we’re at it let’s make sure that we’re compiling to AMD (to be RequireJS friendly) by adding in the following csproj tweaks just before the Microsoft.TypeScript.targets Project import statement:

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<TypeScriptModuleKind>amd</TypeScriptModuleKind>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<TypeScriptModuleKind>amd</TypeScriptModuleKind>
</PropertyGroup>

Where was I? Oh yes, typings. So let’s get the RequireJS typings too:

Install-Package requirejs.TypeScript.DefinitelyTyped

Right – looking at index.html we can see from the data-main tag that the first file loaded by RequireJS, our bootstrapper if you will, is main.js. So let’s add ourselves a main.ts based on John's example (which will in turn generate a main.js):

(function () {
requirejs.config(
{
baseUrl: "scripts",
paths: {
"jquery": "jquery-2.1.0"
}
}
);
require(["alerter"],
(alerter) => {
alerter.showMessage();
});
})();

main.ts depends upon alerter so let’s add ourselves an alerter.ts as well:

define('alerter',
['jquery', 'dataservice'],
function ($, dataservice) {
var
name = 'John',
showMessage = function () {
var msg = dataservice.getMessage();
$('#message').text(msg + ', ' + name);
};
return {
showMessage: showMessage
};
});

And a dataservice.ts:

define('dataservice', [],
function () {
var
msg = 'Welcome to Code Camp',
getMessage = function () {
return msg;
};
return {
getMessage: getMessage
};
});

That all compiles fine. But we’re missing a trick. We’re supposed to be using TypeScripts AMD support so let’s change the code to do just that. First dataservice.ts:

var msg = "Welcome to Code Camp";
export function getMessage() {
return msg;
};

Then alerter.ts:

import $ = require("jquery");
import dataservice = require("dataservice");
var name = "John";
export function showMessage() {
var msg = dataservice.getMessage();
$("#message").text(msg + ", " + name);
}

I know both of the above look slightly different but if you look close you'll see it's really only boilerplate changes. The actual application code is unaffected. Finally, main.ts remains as it is and that's us done; we have ourselves a working demo... Yay!

Thanks to John Papa for creating such a simple demo I could use as the basis for my own demo.

Closing Thoughts#

Unfortunately there is no typing on the alerter reference within main.ts. To my knowledge there is no way to implicitly import the typings here – the only thing you can do is specify them manually. (By the way, if I'm wrong about this then please do set me straight!) That said, this is not so bad really since this main.ts file is essentially just a bootstrapper that kicks things off. All the other files contain the real application code and they have have typings a-go-go. So we're happy.

Finally for bonus points....#

I’ve included the js and js.map files in the project file as they don't seem to be added into the project by Visual Studio when the TS file is created or when it is compiled for the first time. I've also ensured that these files are dependent upon the typescript files they were generated from.

<TypeScriptCompile Include="Scripts\alerter.ts" />
<Content Include="Scripts\alerter.js">
<DependentUpon>alerter.ts</DependentUpon>
</Content>
<Content Include="Scripts\alerter.js.map">
<DependentUpon>alerter.ts</DependentUpon>
</Content>
<TypeScriptCompile Include="Scripts\dataservice.ts" />
<Content Include="Scripts\dataservice.js">
<DependentUpon>dataservice.ts</DependentUpon>
</Content>
<Content Include="Scripts\dataservice.js.map">
<DependentUpon>dataservice.ts</DependentUpon>
</Content>
<TypeScriptCompile Include="Scripts\main.ts" />
<Content Include="Scripts\main.js">
<DependentUpon>main.ts</DependentUpon>
</Content>
<Content Include="Scripts\main.js.map">
<DependentUpon>main.ts</DependentUpon>
</Content>

Want the code for your very own?#

Well you can grab it from GitHub.