Saturday, 17 August 2019

Symbiotic Definitely Typed

I did ponder calling this post "how to enable a good TypeScript developer experience for npm modules that aren't written in TypeScript"... Not exactly pithy though.

Definitely Typed is the resource which allows developers to use TypeScript with existing JavaScript libraries that ship without their own type definitions.

DT began as a way to enable interop between JS and TS. When DT started, everything on npm was JavaScript. Over time it has become more common for libraries (eg Mobx / Angular) to be written (or rewritten) in TypeScript. For publishing, they are compiled down to JS with perfect type definitions generated from the TypeScript alongside the compiled JavaScript. These libraries do not need to exist in Definitely Typed anymore.

Another pattern that has emerged over time is that of type definitions being removed from Definitely Typed to live and be maintained alongside the libraries they support. An example of this is MomentJS.

This week, I think for the first time, there emerged another approach. Kent C Dodds' react-testing-library had started out with the MomentJS approach of hosting type definitions alongside the JavaScript source code. Alex Krolic raised a PR which proposed removing the type definitions from the RTL repo in favor of having the community maintain them at DefinitelyTyped.

I'll directly quote Kent's explanation of the motivation for this:

We were getting a lot of drive-by contributions to the TypeScript typings and many pull requests would either sit without being reviewed by someone who knows TypeScript well enough, or be merged by a maintainer who just hoped the contributor knew what they were doing. This resulted in a poor experience for TypeScript users who could experience type definition churn and delays, and it became a burden on project maintainers as well (most of us don't know TypeScript very well). Moving the type definitions to DefinitelyTyped puts the maintenance in much more capable hands.

I have to admit I was reticent about this idea in the first place. I like the idea that types ship with the package they support. It's a good developer experience; users install your package and it works with TypeScript straight out of the box. However Alex's PR addressed a real issue: what do you do when the authors of a package aren't interested / equipped / don't have the time to support TypeScript? Or don't want to deal with the noise of TypeScript related PRs which aren't relevant to them. What then?

Alex was saying, let's not force it. Let the types and the library be maintained separately. This can and is done well already; React is a case in point. The React team does not work on the type definitions for React, that's done (excellently) by a crew of dedicated React lovers in Definitely Typed.

It's a fair point. The thing that was sad about this move was that the developer experience was going to have more friction. Users would have to yarn add -D @testing-library/react and then subsequently yarn add -D @types/testing-library__react to get the types.

This two step process isn't the end of the world, but it does make it marginally harder for TypeScript users to get up and running. It reduces the developer joy. As a side note, this is made more unlovely by @testing-library/react being a scoped package. Types for a scoped package have a quirky convention for publishing. A fictional scoped package of @foo/bar would be published to npm as: @types/foo__bar. This is functional but non-obvious; it's tricky to discover. A two step process instead of a one step process is a non-useful friction that it would be great to eliminate.

Fortunately, Kent and Daniel K had one of these moments:

Kent suggested that at the same time as dropping the type definitions that were shipped with the library, we try making @types/testing-library__react a dependency of @testing-library/react. This would mean that people installing @testing-library/react would get @types/testing-library__react installed automatically. So from the developers point of view, it's as though the type definitions shipped with the package directly.

To cut a long story short reader, that's what happened. If you're using @testing-library/react from 9.1.2 you're getting Definitely Typed under the covers. This was nicely illustrated by Kent showing what the TypeScript consumption experience looked like before the Definitely Typed switch:

And here's what it looked like after:

Identical! i.e it worked. I grant you this is one of the more boring before / after comparisons there is… But hopefully you can see it demonstrates that this is giving us exactly what we need.

To quote Kent once more:

By adding the type definitions to the dependencies of React Testing Library, the experience for users is completely unchanged. So it's a huge improvement for the maintenance of the type definitions without any breaking changes for the users of those definitions.

This is clearly an approach that's useful; it adds value. It would be tremendous to see other libraries that aren't written in TypeScript but would like to enable a good TypeScript experience for those people that do use TS also adopting this approach.

Update: Use a Loose Version Range in package.json

When I tweeted this article it prompted this helpful response from Andrew Branch of the TypeScript team:

Andrew makes the useful point that if you are adding support for TypeScript via an @types/... dependency then it's wise to do so with a loose version range. In the case of RTL we did it like this:


    "@types/testing-library__react": "^9.1.0"

i.e. Any type definition with a version of 9.1 or greater (whilst still lower than 10.0.0) is considered valid. You could go even looser than that. If you really don't want to think about TypeScript beyond adding the dependency then a completely loose version range would do:


    "@types/testing-library__react": "*"

This will always install the latest version of the @types/testing-library__react dependency and (importantly) allow users to override if there's a problematic @types/testing-library__react out there. This level of looseness is not really advised though. As in the scenario when a library (and associated type definitions) do a major release, users of the old major would get the wrong definitions by default when installing or upgrading (in range).

Probably the most helpful approach is the approach followed by RTL; fixing the major version but allowing all minor and patch releases inside a major version.

Update 2: Further Discussions!

The technique used in this blog post sparked an interesting conversation with members of the TypeScript team when it was applied to https://github.com/testing-library/jest-dom. The conversation can be read here.

Friday, 2 August 2019

Hard-coding a Claim in Development Mode in ASP.Net Core

I was recently part of a hackathon team that put together an API in just 30 hours. We came second. (Not bitter, not bitter...)

We were moving pretty quickly during the hackathon and, when we came to the end of it, we had a working API which we were able to demo. The good news is that the API is going to graduate to be a product! We're going to ship this. Before we can do that though, there's a little tidy up to do.

The first thing I remembered / realised when I picked up the codebase again, was the shortcuts we'd made on the developer experience. We'd put the API together using ASP.Net Core. We're handling authentication using JWTs which is nicely supported. When we're deployed, an external facing proxy calls our application with the appropriate JWT and everything works as you'd hope.

The question is, what's it like to develop against this on your laptop? Getting a JWT for when I'm debugging locally is too much friction. I want to be able to work on the problem at hand, going away to get a JWT each time is a timesuck. So what to do? Well, during the hackathon, we just commented out [Authorize] attributes and hardcoded user ids in our controllers. This works, but it's a messy developer experience; it's easy to forget to uncomment things you've commented and break things. There must be a better way.

The solution I landed on was this: in development mode (which we only use whilst debugging) we hardcode an authenticated user. The way our authentication works is that we have a claim on our principal called something like "our-user-id", the value of which is our authenticated user id. So in the ConfigureServices method of our Startup.cs we have a conditional authentication registration like this:

// Whilst developing, we don't want to authenticate; we hardcode to a particular users id
if (Env.IsDevelopment()) {
    services.AddAuthentication(nameof(DevelopmentModeAuthenticationHandler))
        .AddScheme<DevelopmentModeAuthenticationOptions, DevelopmentModeAuthenticationHandler>(
            nameof(DevelopmentModeAuthenticationHandler), 
            options => {
                options.UserIdToSetInClaims = "this-is-a-user-id"; 
            }
        );
}
else {
    // The application typically uses this
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options => {
            // ...
        });
}

As you can see, we're using a special DevelopmentModeAuthenticationHandler authentication scheme in development mode, instead of JWT. As we register that, we declare the user id that we want to use. Whenever the app runs using the DevelopmentModeAuthenticationHandler auth, all requests will arrive using a principal with an "our-user-id" claim with a value of "this-is-a-user-id" (or whatever you've set it to.)

The DevelopmentModeAuthenticationHandler looks like this:

using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace OurApp
{
    public class DevelopmentModeAuthenticationOptions : AuthenticationSchemeOptions 
    {
        public string UserIdToSetInClaims { get; set; }
    }

    public class DevelopmentModeAuthenticationHandler : AuthenticationHandler<DevelopmentModeAuthenticationOptions> {
        private readonly ILoggingService _loggingService;

        public DevelopmentModeAuthenticationHandler(
            IOptionsMonitor<DevelopmentModeAuthenticationOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock
        ) : base(options, logger, encoder, clock) {
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync() {
            var claims = new List<Claim> { new Claim("our-user-id", Options.UserIdToSetInClaims) };

            var identity = new ClaimsIdentity(claims, nameof(DevelopmentModeAuthenticationHandler));
            var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
            
            return Task.FromResult(AuthenticateResult.Success(ticket));
        }
    }
}

Now, developing locally is frictionless! We don't comment out [Authorize] attributes, we don't hard code user ids in controllers.