Typescript inferring - stop writing tests & avoid runtime-errors. pt1
Jakub Švehla
Posted on November 9, 2020
TLDR:
This is the first chapter of the series where I show you the way how to avoid runtime-errors without writing static types
and tests
using strong Typescript inferring principles.
You can copy-paste source code from examples into your IDE or online Typescript playground and play with it by yourself.
“Minimalist Typescript” chapters:
Inferring (current read)
Introduction
The whole article series is about changing the Typescript mindset on how to use minimalistic static-types in modern Javascript projects. The problem with Typescript is that when programmers discover static-types they start to overuse and over-engineer them. This results in transforming our beloved Javascript into a language similar to C# or Java.
We’re going to try to forget standard type-safe interface best practices where programmers have to create type interface APIs for everything and then implement business logic compatible with these interface declarations. We can see that in the diagram below where two modules (you can also imagine function etc..)communicate via some abstract interface in the middle.
## approach 1
+-------------+
| interface |
+--------+-----+-------+-----------+
| | |
| | |
+-------v----+ | +------v------+
| module 1 | | | module 2 |
| | | | |
+------------+ | +-------------+
|
Ughh… We are Javascript developers and we love dynamic prototyping, that is the reason why the diagram does not look very nice to me. I want to have a type-safe code without runtime errors but at the top of it. I don’t want to write static-types by hand at all. The good news is that Typescript has tools that can help us to “obtain” static-types (known as inferring) from pure Javascript implementation. And that's it. Inferring is the key to this whole Typescript series.
Type Inferring enables the compiler to generate type interfaces in the compile-time and check the correctness of our implementation. We will be able to use inferring for creating logical connections between layers of programming abstraction (like functions/files/and so on).
The final code should be type-safe without writing extra type interface APIs like in the diagram below.
## approach 2
+---------------+ interface 2 +----------------+
| +---------------> | |
| | | |
| module 1 | interface 1 | module 2 |
| | | |
| | <---------------+ |
+---------------+ +----------------+
Our goal is to alter our mindset to think that we will just continue with writing our good old dynamic Javascript. But we will get an extra type-safe layer based on our implementation.
Let’s change the mindset!
Do you remember when you were 15 and started to learn C?
int main() {
int a = 3;
int b = 4;
int c = a + b;
return 0;
}
I don’t like that I have to define that a variable c
is an integer because it’s obvious! Variables a
and b
are integers so a + b
should return integer too!
We can forgive this behavior because C is almost 50 years old and a low-level programming language that is not suitable for quick prototyping in the application layer but It’s fast as hell.
Remove redundant data types
Let’s look at how can we write strongly-typed Javascript and avoid writing redundant type annotations.
First of all, we are going to rewrite the previous C function into Typescript exactly the same way.
const main = (): number => {
const a: number = 3
const b: number = 4
const c: number = a + b
return c
}
Ugh… awful right?
Hmm so let’s apply Typescript “type inference”.
const main = () => {
const a = 3
const b = 4
const c = a + b
return c
}
This looks way better. Typescript is smart and understands that 3
is a number
and plus operator returns a number
.
Type inferring is a Typescript feature that can “obtain” (infer) data types from your code implementation. As you can see in the demo, Typescript checks the code, infers types of variables, and performs static analyses. The beauty of that solution is that 100% of your code is pure Javascript just enhanced by static-type checking.
Advanced Typescript “inferring”
This is a crucial feature that separates Typescript from other type-safe programming languages.
The problem with pure Javascript started with an escalating number of lines of code. Your brain (and unit tests
😃) is just a thin layer that has to check if your newly implemented refactored data structures are compatible with the rest of your code. When you are done with your code you have to check that your documentation is compatible with your latest implementation.
Typescript can fully work like your brain and perform static analyses of code without extra typing by hand. For example, you may write code like:
const foo = ({ bar, baz }) => [bar, baz]
You as a programmer have no idea what type of bar
and baz
are. Obviously, Typescript has no idea about that as well.
Let’s compare the previous example with the next one:
const main = () => {
const bar = 3
const baz = 4
const foo = { bar, baz }
return [foo.bar, foo.baz]
}
It’s much clearer in this more “spaghetti-like” implementation. Variables foo
and bar
are just numbers
.
Don’t forget that if your code contains many “redundant” layers of abstraction, code readability rapidly decreases. In the first example, our brain had no idea what variables bar
and baz
were.
Many people start to get frustrated with incomprehensible, unclear code, and start to write functions with type interfaces like this one:
type FooArg = {
bar: number,
baz: number
}
const foo = ({ bar, baz }: FooArg) => [bar, baz]]
In this example, we add an extra 4 lines just for typing an interface of the foo
micro function. Then the code grows, the codebase starts to get less flexible and you have just lost the flexibility of Javascript.
The best statically analyzed code without type defining is the spaghetti one!
— — Jakub Švehla — —
Skip redundant interface definition — use typeof
Do you know the DRY (Don’t repeat yourself) programming philosophy?
Every time you create a type interface with defined keys and so on, you start to duplicate your code (and one cat will die).
const user = {
id: 3,
name: 'Foo'
}
vs
type User = {
id: number
name: string
}
const user: User = {
id: 3,
name: 'Foo'
}
We can solve this issue with the Typescript typeof
type guard, which takes a Javascript object and infers data types from it.
const user = {
id: 3,
name: 'Foo'
};
type User = typeof user
You can see that this new code does not create declaration duplicates and our Javascript object is the source of truth for the type User
. And at the top of it, we can still use Typescript types to check the correctness of the code implementation.
The next example demonstrates how type-checking finds an issue in the code using just 2 lines of Typescript code.
const user = {
id: 3,
name: 'Foo'
};
type User = typeof user
const changeUserName = (userToEdit: User, age: number) => {
userToEdit.name = age;
};
If Typescript is not able to 100% correctly infer your static types, you can help the compiler by defining a sub-value of an object with as
syntax. In this example: state: 'nil' as 'nil' | 'pending' | 'done'
we set that the state attribute contains only nil
, pending
or done
value.
const user = {
id: 3,
name: 'Foo',
// Help the compiler to correctly infer string as the enum optional type
state: 'nil' as 'nil' | 'pending' | 'done'
};
type User = typeof user
const changeUserName = (useToEdit: User, newName: string) => {
useToEdit.name = newName;
useToEdit.state = 'pendingggggg';
};
as you can see:
the only place where you have to define static types by hand are just function arguments
and the rest of the code can be inferred by the Typescript compiler. If you want to be more strict with inferring you can help the Typescript compiler by using the as
keyword and write a more strict type inferring Javascript code.
You should not forget that a newly defined function outside of the previous function scope is just another “reusable” layer of abstraction which decreases compiler & human readability and simplicity.
Algebraic data type — Enumerated values
One of the best features of Typescript is Pattern matching
based on enumerated values.
Let’s have 3 types of animals. Each kind of animal has different attributes. Your target is to create the custom print function differently for each of your animals.
Your data model layer could look like:
const elephantExample = {
trunkSize: 10,
eyesColor: 'red'
}
const pythonExample = {
length: 50
}
const whaleExample = {
volume: 30
}
First of all, we can simply get static types from values by using the typeof
keyword.
type Elephant = typeof elephantExample
type Python = typeof pythonExample
type Whale = typeof whaleExample
type Animal =
| Elephant
| Python
| Whale
Let’s add a type
attribute for each of our animals to make a unique standardized way of identifying an “instance” of the animal type and check the correctness of objects.
// & operator merge 2 types into 1
type Elephant = typeof elephantExample & { type: "Elephant" }
type Python = typeof pythonExample & { type: "Python" }
type Whale = typeof whaleExample & { type: "Whale" }
type Animal =
| Elephant
| Python
| Whale
const animalWhale: Animal = {
type: "Whale",
volume: 3
}
const animalWhaleErr: Animal = {
length: 100,
type: "Whale",
}
You can see that we use the Typescript &
operator for merging two Typescript’s data types.
Now we can create a print function that uses a switch-case
pattern matching over our inferred javascript object.
const elephantExample = {
trunkSize: 10,
eyesColor: 'red'
}
const pythonExample = {
length: 50
}
const whaleExample = {
volume: 30
}
// & operator merge 2 types into 1
type Elephant = typeof elephant & { type: "Elephant" }
type Python = typeof python & { type: "Python" }
type Whale = typeof whale & { type: "Whale" }
type Animal =
| Elephant
| Python
| Whale
const printAnimalAttrs = (animal: Animal) => {
// define custom business logic for each data type
switch (animal.type) {
case 'Elephant':
console.log(animal.trunkSize)
console.log(animal.eyesColor)
break
case 'Python':
console.log(animal.size)
break
case 'Whale':
console.log(animal.volume)
break
}
}
As you see in this example, we just took a simple Javascript code and added a few lines of types for creating relations between data structures and function arguments. The beauty of that solution is that Typescript does not contain business logic or *data shape declaration so Javascript code is **the only source of truth*. Typescript still checks 100% of your source code interface compatibility and adds a nice self-documentation feature.
Use as const
for constant values
Typescript has an as const
syntax feature that helps with defining constant values instead of basic data types. If the Typescript compiler found an expression like:
it obviously infers justifyContent
key as a string
. But we as programmers know that justifyContent
is an enum with values:
'flex-start' | 'flex-end' | 'start' | .. | .. | etc ...
We have no option of getting this justifyContent
data-type information from the code snippet because CSS specification is not related to the Typescript specification. So let’s transform this static object into a type with exact compile-time values. To do this, we are going to use an as const
expression.
Now we can use justifyContent
as a readonly
constant value flex-start
.
In the next example, we combine as const
, as
, and typeof
for a one-line configuration type interface.
Conclusion
In this chapter, we went through the basics of Typescript smart inferring. We used Typescript as a type-safe glue for our Javascript code. We were also able to get perfect IDE help and documentation with a minimal amount of effort.
We learned how to:
Infer and check basic data types.
Add static types for arguments of a function.
Use
typeof
for inferring Typescript types from a static Javascript implementation.Merge type objects with
&
operator.Make options types with
|
operator.Use
switch-case
pattern matching on different data types.Use
as {{type}}
for correcting inferred data types.Use
as const
for type values.
Next chapter:
- In chapter 2, we will look at more advanced type inferring and type reusing with Typescript generics. In the second part of the article, we will declare custom generics for “inferring” from external services.
If you enjoyed reading the article don’t forget to like it to tell me that it makes sense to continue.
Posted on November 9, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.