My confusions about TypeScript

Ken Bellows - Jan 3 '20 - - Dev Community

I have heard endless good things about TypeScript in the last couple years, but I've never really had a chance to use it. So when I was tasked with writing a new API from scratch at work, I decided to use the opportunity to learn TypeScript by jumping into the deep end.

So far, here are my positive takeaways:

  • I'm a huge fan of the added intellisense in my IDE (VS Code). I've always found the intellisense for regular JavaScript packages to be a bit flaky for some reason, but it's rock solid with TypeScript.
  • The "might be undefined" checks have definitely saved me some time by pointing out places where I need to add a few null checks after .get()ing something from a Map, etc.
  • I have always liked being able to spell out my classes in JavaScript; I've often gone to extreme lengths to document JS classes with JSDoc.

But I've run into a few significant frustrations that have really slowed me down, and I'm hoping some of my much more experienced TypeScript DEV friends will be able to help me figure them out! 😎

Class types

I can't figure out how to use or declare class types, especially when I need to pass around subclasses that extend a certain base class. This came up for me because I'm using Objection.js, an ORM package that makes heavy use of static getters on classes. I need to pass around subclasses of Objection's Model class to check relationship mappings and make queries, so I need a way to say, "This parameter is a class object that extends Model". I wish I had something like:

function handleRelations(modelClass: extends Model) ...

The best I've found so far is to use a rather annoying interface and update it every time I need to use another method from Objection's extensive API, like:

interface IModel {
  new(): Model
  query(): QueryBuilder
  tableName: string
  idColumn: string | string[]
  relationMappings: object
  // etc.
}

function handleRelations(modelClass: IModel) ...

This works, but it's rather annoying to have to reinvent the wheel this way. Is there a more explicit way to tell TypeScript, "I mean a class extending this type, not an instance of this type"?

Overriding methods with different return types

This is more a best-practice question than anything else. I've run into some cases where a base class declares a method that returns a particular type, but subclasses need to override that method and return a different type. One example is the idColumn static getter used by Objection models, which can return either a string or a string[].

I've found that if I simply declare the base class as returning one type and the subclass as returning another, I get yelled at:

class Animal extends Model {
  static get idColumn(): string {
    return 'name'
  }
}

class Dog extends Animal {
  static get idColumn(): string[] {
    return ['name', 'tag']
  }
}
/* ERROR
Class static side 'typeof Dog' incorrectly extends base class static side 'typeof Animal'.
  Types of property 'idColumn' are incompatible.
    Type 'string[]' is not assignable to type 'string'.
*/

If I declare the base class with a Union type, that seems to work, although adding another layer of subclass trying to use the original base class's type no breaks because of the middle class:

class Animal extends Model {
  static get idColumn(): string | string[] {
    return 'name'
  }
}

class Dog extends Animal {
  static get idColumn(): string[] {
    return ['name', 'tag']
  }
}

class Poodle extends Dog {
  static get idColumn(): string {
    return 'nom'
  }
}
/*
Class static side 'typeof Poodle' incorrectly extends base class static side 'typeof Dog'...
*/

So I'm now torn. I like to be as specific as I can in my method signatures, but it seems I have two choices here: either always use the full union type string | string[] as the return type of the idColumn getter for all subclasses, or simply don't declare a return type for subclasses, only the base class:

class Animal extends Model {
  static get idColumn(): string | string[] {
    return 'name'
  }
}

class Dog extends Animal {
  // this?
  static get idColumn(): string | string[] {
    return ['name', 'tag']
  }

  // or this?
  static get idColumn() {
    return ['name', 'tag']
  }
}

So my question here is, which is better? Is there an accepted paradigmatic solution here? I don't really like either; the former feels slightly misleading, but the latter feels incomplete. I'm leaning toward the latter in this case since it's immediately obvious what the type of a constant return value is, but in more complex cases involving an actual method with some complicated logic, I'm not sure how I'd handle it.

Dealing with simple objects

Okay, this is a more minor annoyance, but it really bugs me. If I just want to say, "This function accepts/returns a plain ol' object with arbitrary keys and values", the only syntax I can find is:

{ [key: string] : any }

Used once on it's own, that's not the worst thing I've ever seen, but I have a method that accepts an object-to-object Map and returns another one, and the method signature looks like this:

function converter(input: Map<{ [key: string] : any }, { [key: string] : any }>): Map<{ [key: string] : any }, { [key: string] : any }>

That's... that's not okay. I've run into more complex example as well, cases where I'm declaring interfaces with nested objects and such, and this syntax makes them near impossible to read. So my solution has been to declare a trivial interface called SimpleObject to represent, well, a simple object:

interface SimpleObject {
  [key: string] : any
}

And like, that works, but whenever I show anyone my code I have to explain this situation, and it just seems like an oversight that there's apparently no native name for simple objects in TypeScript. Am I missing something?

Conclusion

Thanks to anyone who took the time to read this, and thanks a million to anyone who helps me out or leaves a comment! I'm enjoying TypeScript on the whole, and I'm sure little quirks like this will become natural after a while, but if there is a better way to handle them, I'd love to know! 😁

. . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player