Saturday, March 2, 2019

I saw Joe DeCock of ILM Services speak on TypeScript at the Twin Cities .NET User Group on Thursday night which is also hosted at ILM Services.

Did I mention that I work at ILM Services too? Anyhow, the talk had to do with some of the hoops the language has to jump through to introduce comparable to C#, compiler-safe capabilities to its subset of JavaScript. Arrays look like this under the hood:

interface Array<T> {
   length: number;
   
//lots of other methods here
   reverse(): T[];
   [n: number]: T;
}

 
 

[n: number] has a sister [s: string] which can be used to fish stuff out of a dictionary. Indeed, Lodash's dictionary works in this manner in TypeScript. The Index Type Query Operator uses the keyof keyword in tandem with the extends keyword like so:

get<T, Key extends keyof T> (
   object: T,
   path: Key
): T[Key];

 
 

Use this stuff like so:

get(someComplexObject, 'someProperty');

 
 

This will fish out a property from an object in another dictionaryesque trick. Joe DeCock spent some time explaining how extends and extends keyof are similar and different with a Venn diagram at one of his slides. This is inheritance and if you inherit from T with extends in classic inheritance or if you inherit from T with extends keyof, in both cases you are making a type that is a more-specific version of T. What is different is that while extends is going to allow you to add properties to your more-specific T and make it fatter, extends keyof subtracts stuff, reducing T to just what you want from it. In the example immediately above we are reducing T to its someProperty property. A good chunk of the talk went into TypeScript's very-different-from-C# flavor of method overloading. The only way this can work is by handing in a union type and then having different workflows inside of a method or other function based on what was handed in. (There will be precious little you can do with a union type without breaking back out what it is specifically beyond .toString() and .valueOf() after all.) Alright, so if you really wanted two methods named the same thing wherein one took a Date and one took a string and you are settling for handing in to a single method a Date | string, how can you make a distinction when you need it? There are really two ways to do this and one is better than the other.

  1. if ((<Date>myBirthday).getFullYear) { is a compiler-safe way to see if you may call .getFullYear() on the thing you handed in, but perhaps a better way to go is...
  2. if (myBirthday instanceof Date) {
    This is an example of "narrowing" with the instanceof keyword which may be used with types. Narrowing with a primative such as a string uses the typeof keyword like so in contrast:
    if (typeof myBirthday === 'string') {

What if we don't just want to hand in different things, but we also want the return type to vary based upon what we hand in? Well, we probably don't want to just return a union type in those scenarios as it is inconvenient. Every time we used our method we would have to write some logic like what is above to make sense of what it returned after all. That's not user-friendly at all. The solution is to use a User Defined Type Guard or two or three like so:

function identity(x:string): string;
function identity(x:number): number;
function identity(x: string | number): string | number {
   return x;
}

 
 

The more complicated your TypeScript gets the more it leads to unintelligible errors. When you find yourself in these scenarios, progressively walk your code downwards to a dumber and dumber more simplistic crafting until the error messages make sense.

No comments:

Post a Comment