Or, how not to make the mistakes I made with Typescript.
As a long-time .NET developer, I took the plunge into TypeScript a couple of years ago after much uhm-ing and ah-ing. I learned that you could write interfaces in TypeScript, and I immediately starting doing them wrong.
In .NET, an interface is something that you expect to be implemented in a class. For example:
public interface IFoo { string Bar { get; } string Baz(string bar); } public class FunkyFoo { public string Bar { get; set; } public void Baz(string bar) => $"Hello {bar}"; }
You might code against the interface, but you would only really create interfaces if you expected several different concrete classes to implement that contract.
When I started writing TypeScript, I wrote interfaces in much the same fashion:
interface IFoo { string Bar; Baz(bar: string); } class FunkyFoo { public Bar: string; public Baz(bar: string): string { return `Hello ${bar}`; } }
The problem is that, while this works, it’s not really giving you the full benefit of TS interfaces. TypeScript, being a super-set of JavaScript, is a functional language – unlike .NET, the classes aren’t the most important thing in it. Using the approach above, I was writing code like:
let foo: Array<IFoo>; $.getJSON("foo/bar", json => { for (let i = 0; i < json.length; i++) { foo.push(new FunkyFoo(json[i])); } }
Ugh. Horrible and messy. If I’d used interfaces properly, I’d have something like this:
interface Foo { bar: string; baz: string; } // ... let foo: Array<Foo>; $.getJSON("foo/bar", json => { foo = json; });
The realisation that dawned on me was that interfaces are just data contracts – nothing more. Due to the loose typing of JavaScript, I could quite happily say that an object was of type Foo, without doing any conversion.
So, what should I have been using interfaces for? Well, firstly, data contracts as seen above – basically just a way to define what data I expect to get from/send to the server. Secondly, for grouping sets of repeated parameters, as in:
function postComment(email: string, firstName: string, lastName: string, comment: string) { // etc } // becomes: interface UserInfo { email: string; firstName: string; lastName: string; } // now the method is much cleaner, // and we can re-use these parameters function postComment(user: UserInfo, comment: string) { // etc }
I’ve ended up thinking of interfaces as more akin to records in F# – just basic DTOs. Doing this has actually made my code better – now I have a clean separation between data and functionality, rather than mixing both in the same structure.