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.