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.