Categories


Archives


Recent Posts


Categories


Reflection and Types in TypeScript

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

In traditional, “plain Blain” style javascript, it’s relatively simple to access an object’s property via a string

// prop-of.ts
class Foo {
    prop1 = 'foo'
    prop2 = 'baz'
    prop3 = 'bar'
}

const foo = new Foo()

// using a string constant
console.log(foo['prop1'])

// using a string's variable
const prop2 = 'prop2'
console.log(foo[prop2])

// using a string popped off on array of string
const props = ['prop1','prop2','prop3']
let prop3 = props.pop()
console.log(foo[prop3])

Run this program, and NodeJS won’t complain.

$ node prop-of.ts
foo
baz
bar

However, if you try this same thing with TypeScript (javascript’s grown up corporate sibling), TypeScript is not happy.

# This happen when you're running with
#     "noImplicitAny": true
# https://www.typescriptlang.org/docs/handbook/compiler-options.html

$ ts-node prop-of.ts
//...
prop-of.ts:19:13 - error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Foo'.
  No index signature with a parameter of type 'string' was found on type 'Foo'.

19 console.log(foo[prop3])
//...

What surprised me about this wasn’t that TypeScript was upset with this kind of access, but that it was only upset with it sometimes. TypeScript was fine when I did this

// using a string constant
console.log(foo['prop1'])

// using a string's variable
const prop2 = 'prop2'
console.log(foo[prop2])

However, this third code snippet made TypeScript upset

// using a string popped off on array of string
const props = ['prop1','prop2','prop3']
let prop3 = props.pop()
console.log(foo[prop3])

TypeScript didn’t want to use a string popped off of an array to access an object property, but was fine with a string constant or a string in a variable.

This threw me. For someone who learned to program when I did, it seems like TypeScript either should, or should not, allow this sort of access.

TypeScript’s Compiler is your Guardian Angel

It turns out when you try to access an object’s property via a string, TypeScript’s compiler is still looking out for you. If the compiler can’t determine what’s inside of your string variable, it will refuse to compile your program.

So here

console.log(foo['prop1'])

when we put 'prop1' into our program, that’s a string constant. The compiler knows its value, and can check the Foo type/class to make sure there’s a key named `prop1“

It’s similar when we use a const variable

// using a string's variable
const prop2 = 'prop2'
console.log(foo[prop2])

Because the prop2 variable is a const, the compiler knows it should always contain the value prop2. The compiler can check the Foo type to make sure there’s a key named prop2, which there is, so it’s happy. If we had tried with a let or a var

// neither of these will compile
{
    let prop2 = 'prop2'
    console.log(foo[prop2])
}
//...
{
    var prop2 = 'prop2'
    console.log(foo[prop2])
}

TypeScript would have refused to compile the program, because the value of prop2 might change.

Here’s our third code sample

const props = ['prop1','prop2','prop3']
let prop3 = props.pop()
console.log(foo[prop3])

Although props is declared as a const, in javascript const only ensures that the variable reference remains constant. Our props variable will always point at the same array, but that array might have new values added to it. Because the contents of the array might change, TypeScript’s compiler can’t guarantee the values from this array are keys of Foo, so it won’t compile the program.

Humans vs. Computers

If we look at this program as a human

const props = ['prop1','prop2','prop3']
let prop3 = props.pop()
console.log(foo[prop3])

We can say (given the current rules of javascript) that the prop3 variable will have a key of the Foo class

class Foo {
    prop1 = 'foo'
    prop2 = 'baz'
    prop3 = 'bar'
}

//...

/* A. */ const props = ['prop1','prop2','prop3']
/* B. */ const prop3 = props.pop()
/* C. */ console.log(foo[prop3])

We see the array declared in one line, we see prop3 declared in the next, and then we see the access. Javascript and TypeScript aren’t threaded environments — there’s nothing that can come along and change the value of props between that line A and line B. As a human, it’s easy to look at this code and say “yeah, that’s cool”.

For the compiler though, that’s less easy. All it has to rely on is the types involved. Since the compiler can’t reason about the program itself, it has to reject anything that might be a problem.

Typed Meta Programming is a Complicated Profession

There were two solutions in the answer — both introduced me to TypeScript features I wasn’t aware of.

The first was using a feature called const assertions.

const props = ['prop1','prop2','prop3'] as const
const prop3 = props[props.length-1]
console.log(foo[prop3])

Syntactically this looks redundant — a const as a — const? What does that even mean?

The trailing as const makes the value of props constant. In other words, the first const makes sure the props variable can’t be reassigned. The second as const makes sure that values can’t be added or removed from the array — that’s why we needed to replace our pop() with some code to access the last array element, (props[props.length-1])

With as const in place, the TypeScript compiler knows that props will only ever contain 'prop1','prop2','prop3', which are all keys of the Foo type/class.

The second solution is, instead of using an array of strings — we use an array
of “keyof Foo” values.

const props:(keyof Foo)[] = ['prop1','prop2','prop3']
const prop3 = props.pop()
console.log(foo[prop3])

There’s a lot going on in this one. First, we’ve given our array a type. An array of strings would look like this

const props:string[] = ['prop1','prop2','prop3']

The :string[] indicating that the variable should be an array ([]) of strings (string).

However, what we’ve done in the prior code snippet is create an array of “keys of the Foo Class”, via the keyof Foo syntax

... props:(keyof Foo)[] = ...

What this means is we can add anything to the props array, so long as that thing is a key of the Foo class/type. The TypeScript manual has more on this keyof type although be warned, you’re heading into the deep end of generics and advanced types.

It’s interesting how much effort has been put into keeping TypeScript as dynamic as javascript, while still offering type safety. I have no idea if this tradeoff is worth it — so far I’ve had to spend a lot of time with rewiring my own assumptions and learning all the corner cases of the type system just to get basic things done.

Copyright © Alan Storm 1975 – 2020 All Rights Reserved

Originally Posted: 16th March 2020