Getters and setters (also known as accessors) were introduced to JavaScript when ECMAScript 5 (2009) was released. The thing is, there's a lot of confusion about their utility and why you would ever even want to use them. I came across this reddit thread where the discussion was about if they were an anti-pattern. Note: this story goes into some advanced TypeScript. I’m going to assume you know what decorators, type aliases and generics are. How do I create a mapped type that extracts only the properties that have been annotated by my property decorator? This question is a little abstract so let’s look at it through an example.
TypeScript 2.8's conditional types can be used to create compile-time inference assertions, which can be used to writetests that verify the behavior of TypeScript's inference on your API.
This is a very powerful tool for improving the usability of your API. To demonstrate, let's imagine that we are buildinga 'pluck' function:
TypeScript has abstract classes, which are classes that have partial implementation of a class and in which other classes can be derived from. They can’t be instantiated directly. Unlike interfaces, abstract classes can have implementation details for their members. To declare an abstract class, we can use the abstract keyword.
While this may look like a perfectly good type signature and implementation, when we consider the usability of thereturned value's type, there are going to be some surprises—especially in TypeScript's --strict
mode.
For this example, let's assume we have the following interface:
If we use this naive version of pluck
, we'll see that there are some unexpected consequences of type inference.
Even though the intent of the API is to return a structure that's a subset of the plucked object, it has two unintendedusability consequences with TypeScript's inference behavior:
- The returned object has members are all of the type
T | undefined
. This will cause frustrations when using thispluck
function in--strict
mode. - Keys that are not specified are optionally present in the returned object's type. We should be able to know thatthe
bool
key will never be present in the return type.
How can we verify compile-time inference behavior?
Typescript Abstract Property Default Value
If we wanted API usability/behavior to act a certain way at runtime, we could write a few tests which assert thatbehavior and then modify our implementation of pluck
so that our desired behavior is verified. However, since thebehavior we want is something that is determined at compile-time, we need to resort to telling the compiler toperform these assertions for us at compile-time.
Using TypeScript 2.8's conditional types, we can define the shape and inference behavior of the API we want to buildprior to actually implementing it. Think of this as a sort of TDD for your types.
We can do this by (1) asserting the inferred value is assignable to the types that we want (conditional types comein handy here), and (2) cause the compiler to reject code at compile time when these assertions are not true.
Typescript Abstract Property Management
As a tiny example, if we want to write a compile-time test that asserts 'this value should really be inferred as anumber,' we can do the following:
Using these assertions to make a better pluck
Applying this technique to our API, we can describe the behavior we want for our case #1 (members having an unwanted | undefined
):
Excellent, now that we have a compile-time error that asserts our behavior, we can redefine pluck
's type signature to bemore accurate.
This compiles, which means our problem #1 is solved! Unfortunately, this signature is a lie. While we 'fixed' #1, westill need to deal with our case #2, where missing members are still present in the returned type.
To check for this, we need a few type devices to fail compile if a key is present in a type:
Asserting the absence of a key
There are a few type operations that we need to know in order to check if an object does not have a key.
First off, here's a brief refresher on the building blocks we'll use:
So let's build a type device that evaluates to true
when an object T
does not have a key K
:
Putting it all together
Now with this TrueIfMissing
type device, we can assert that we do not want to have certain keys present in thereturned object from our pluck
:
Finally we can create a version of pluck that satisfies all of our usability concerns:
Why go through all this work?
When we have automated tests which assert the behavior of our code, we gain confidence that changes to our software willnot introduce regressions. However, when designing an API which is meant to leverage type inference to gain usability,there hasn't really been an obvious way of doing this.
This technique allows us to effectively test how TypeScript performs its inference for users of our API. We canbuild a test module which makes assertions about our desired type inference, and if the test file compiles successfully,our assertions are correct! That way, if our API subtly changes in a way that makes return values or callback parametersharder to infer, we can be alerted to this by a failure to compile.
Typescript Abstract Class Property
If you happen to know of other techniques that can be used to accomplish this sort of compile-time assertion, I'd loveto hear them! Please reach out and let me know!