James Kerr

TypeScript Classes Are Giving Me Carpal Tunnel

Whenever a class needs a few arguments in TypeScript, I cringe because I know I’m going to need to perform a ceremony to make it happy.

Let’s start with the simple case. If a Class needs two arguments, I’d do this:

class Point {
  x: number
  y: number
  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }
}

There is some repetition to get the types and initialization right. Fortunately, the TypeScript authors created a shorthand syntax to calm this down.

class Point {
  constructor(public x: number, public y: number) {}
}

Very nice. By providing a typescript keyword public, private, protected, or readonly before the argument name, the class will make it a member variable and initialize it with the argument for you!

This is amazing but…

new GiantObjectWithManyArgs(field, table, null, null, undefined, "init", () => console.log("callback"))

…it breaks down when you have more than 1 or 2 arguments. To fix this, a common pattern is to pass a single object argument.

new GiantObjectWithManyArgs({
  field: myField,
  table: myTable,
  parent: null,
  children: null,
  status: "init",
  onComplete: () => console.log("callback")
})

This is so much better than positional arguments.

  1. They have names!
  2. The order doesn’t matter!
  3. The optional args can be omitted!

In JavaScript, this would be a piece of cake to setup in the constructor.

class GiantObjectWithManyArgs {
	constructor(args) {
	  Object.assign(this, args)
	}
}

Everything in the args object becomes a member of the class. But in TypeScript, the amount of code needed to make this work explodes!

This is the least verbose way I know to write it.

type Args = {
  field: zed.Field
  table: zed.Table
  parent: GiantObject | null
  children: GiantObject[] | null
  status: "init" | "complete"
  onComplete?: () => void  
}

class GiantObject {
	field: Args["field"]
	table: Args["table"]
	parent: Args["parent"]
	children: Args["children"]
	status: Args["status"]
	onComplete: Args["onComplete"]
	
	constructor(args: Args) {
	  this.field = args.field
	  this.table = args.table
	  this.parent = args.parent
	  this.children = args.children
	  this.status = args.status
	  this.onComplete = args.onComplete
	}
}

I’ve got blisters on my fingers!

TypeScript authors…you saw the the need for positional argument shorthand. I’m sure you see the need for object argument shorthand as well.

I’m no language designer so here is my blind stab in the dark for syntax ideas.

type Args = {
  field: zed.Field
  table: zed.Table
  parent: GiantObject | null
  children: GiantObject[] | null
  status: "init" | "complete"
  onComplete?: () => void  
}

// Not Real TypeScript
class GiantObject {
  constructor(public assign args: Args) {}
	
  // Aww, it's probably hard to specifiy 
  // some as a private and others as public...
}
// Or mabye
class GiantObjects assigns Args {
  constructor(args: Args) {
    for (let key in args) this[key] = args[key]
  }
}

Workaround

My workaround for this is to assign the whole object to a member variable called “args”.

type Args = {
// ...
}

class GiantObject {
	args: Args
	
	constructor(private args: Args) {}
}

Then I have to make getters for each of the pieces I need. It’s not a bad way to go, but I’m coding around something because the tooling makes the preferred style difficult.

Object arguments are so much better than positional arguments, but the boilerplate in TypeScript compared to plain JavaScript makes them almost not worth it.

Comments

Please email your thoughts to my personal address. kerrto-prevent-spam@hto-prevent-spamey.comto-prevent-spam

I would love to hear from you.