marp | theme | paginate |
---|---|---|
true |
rose-pine-moon |
skip |
How to improve your typescript code by learning other languages.
- I'm Andre :D
- Senior Software Engineer at Interhyp
- Fullstack with more experience in the frontend
- I love all sorts of languages!
Learning another language is not only learning different words for the same things, but learning another way to think about things. — Flora Lewis
- 64% Javascript
- 39% Typescript
- 13% Rust
- 9% Kotlin
- 2% Elixir
- 2.4m Javascript
- 296k Rust
- 129k Typescript
- 82k Kotlin
- 30k Elixir
3 billion devices run Java every day!
Java inspired me to dabble with Kotlin though.
Kotlin was developed by Jetbrains as an easily implementable, interoperable replacement for Java.
Example:
Given a team of pokémon, calculate the total sum of all levels.
const team = [
{ name: "Pikachu", level: 20, trainer: "Ash" },
{ name: "Charmander", level: 12, trainer: "Gary" },
{ name: "Snorlax", level: 42, trainer: "Ash" }
]
team.reduce((acc, { level }) => acc + level, 0)
// result: 74
team.sumOf { it.level }
So comfy :D
Given a team of pokémon, group them by their trainer.
const team = [
{ name: "Pikachu", level: 20, trainer: "Ash" },
{ name: "Charmander", level: 12, trainer: "Gary" },
{ name: "Snorlax", level: 42, trainer: "Ash" }
]
const result = team.reduce((acc, pokemon) => {
acc[pokemon.trainer] = acc[pokemon.trainer]?.concat(pokemon) ?? [pokemon];
return acc
}, {})
{
"Ash": [
{
"name": "Pikachu",
"level": 20,
"trainer": "Ash"
},
{
"name": "Snorlax",
"level": 42,
"trainer": "Ash"
}
],
"Gary": [
{
"name": "Charmander",
"level": 12,
"trainer": "Gary"
}
]
}
team.groupBy { it.trainer }
This stuff is really easy!
There are many libraries trying to compensate this lack of comfort functions like lodash
Using lodash:
import _ from 'lodash';
const team = [
{ name: "Pikachu", level: 20, trainer: "Ash" },
{ name: "Charmander", level: 12, trainer: "Gary" },
{ name: "Snorlax", level: 42, trainer: "Ash" }
]
_.groupBy(team, 'trainer');
This is actually such a common usecase, that Typescript will include it in the future as well.
If you're interested feel free to follow the official proposal
Let's write a function that removes all pokémon that are not by Ash.
const ashFilter = (pokemon: Pokemon[]) =>
pokemon.filter(({ trainer }) => trainer === "Ash")
Let's write another function that removes all pokémon that are lower than level 30.
const levelFilter = (pokemon: Pokemon[]) =>
pokemon.filter(({ level }) => level >= 30);
Now let's combine them!
levelFilter(ashFilter(team));
It is super easy to extend stuff in Kotlin!
fun List<Pokemon>.ashFilter() = this.filter{ it.trainer == "Ash" }
fun List<Pokemon>.levelFilter() = this.filter{ it.level >= 30 }
team
.ashFilter()
.levelFilter()
I love it <3
function ashFilter(this: Pokemon[]) {
return this.filter(({ trainer }) => trainer === "Ash");
}
function levelFilter(this: Pokemon[]) {
return this.filter(({ level }) => level >= 30);
}
Array<Pokemon>.prototype.ashFilter = ashFilter;
Array<Pokemon>.prototype.levelFilter = levelFilter;
team
.ashFilter()
.levelFilter()
declare global {
interface Array<Pokemon> {
ashFilter(): Pokemon[];
levelFilter(): Pokemon[]
}
}
And it's heavily frowned upon :(
Just create a wrapper class:
class PokemonList extends Array<Pokemon> {
ashFilter(this: PokemonList) {
return new PokemonList(...this.filter(({ trainer }) => trainer === "Ash"));
}
levelFilter(this: PokemonList) {
return new PokemonList(...this.filter(({ level }) => level >= 30));
}
}
//...
team
.ashFilter()
.levelFilter();
type PokemonType = 'FIRE' | 'WATER';
const fight = (attacker: PokemonType, defender: PokemonType) => {
if(attacker == "FIRE" && defender == "FIRE") return "Attacker wins!"
if(attacker == "FIRE" && defender == "WATER") return "Defender wins!"
if(attacker == "WATER" && defender == "FIRE") return "Attacker wins!"
if(attacker == "WATER" && defender == "WATER") return "Attacker wins!"
}
Sometimes simple code is the best code!
We could use switch cases:
const fight = (attacker: PokemonType, defender: PokemonType) => {
switch(attacker) {
case "FIRE": switch(defender) {
case "FIRE": return "Attacker wins!"
case "WATER": return "Defender wins!"
}
case "WATER": switch(defender) {
case "FIRE": return "Attacker wins!"
case "WATER": return "Attacker wins!"
}
}
}
fun fight(attacker: PokemonType, defender: PokemonType): String {
when (attacker) {
FIRE -> when (defender) {
PokemonType.FIRE -> return "Attacker wins!"
PokemonType.WATER -> return "Defender wins!"
}
PokemonType.WATER -> when (defender) {
PokemonType.FIRE -> return "Attacker wins!"
PokemonType.WATER -> return "Attacker wins!"
}
}
}
fun fight(attacker: PokemonType, defender: PokemonType): String = when (attacker) {
FIRE -> when (defender) {
FIRE -> "Attacker wins!"
WATER -> "Defender wins!"
}
WATER -> when (defender) {
FIRE -> "Attacker wins!"
WATER -> "Attacker wins!"
}
}
I love the way Kotlin handles expressions!
When I add a new type Rock, I get the following compile error:
'when' expression must be exhaustive. Add the 'ROCK' branch or an 'else' branch.
"rules": {
"@typescript-eslint/switch-exhaustiveness-check": "error"
}
import {match} from 'ts-pattern'
const fight = (attacker: PokemonType, defender: PokemonType) => {
return match(attacker)
.with("FIRE", () => match(defender)
.with("FIRE", () => "Attacker wins!")
.with("WATER", () => "Defender wins!")
)
.with("WATER", () => match(defender)
.with("FIRE", () => "Attacker wins!")
.with("WATER", () => "Attacker wins!")
)
.exhaustive()
.run();
}
Let's start with a simple example!
let v = vec![1, 2, 3];
v.push(4)
error[E0596]: cannot borrow `v` as mutable, as it is not declared as mutable
--> src/main.rs:6:5
|
6 | v.push(4);
| ^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
4 | let mut v = vec![1, 2, 3];
| +++
For more information about this error, try `rustc --explain E0596`.
Informative, pretty and concise!
But we can fix this by using Pretty Typescript Errors!
Rust is immutable by default.
The correct way would have been:
let mut v = vec![1, 2, 3];
v.push(4)
let list: readonly number[] = [1, 2];
list.push(3);
> Property 'push' does not exist on type 'readonly number[]'.ts(2339)
let list: readonly number[] = [1, 2];
list.push(3);
console.log(list); // => `[1, 2, 3]`
Don't forget, all this type stuff is purely fictional!
class ImmutableArray<T> extends Array<T> {
override push(...args: never): never {
throw Error("This is Immutable!");
}
override pop(...args: never): never {
throw Error("This is Immutable!");
}
}
const list = new ImmutableArray(1,2);
list.push(1); //type error & throws
There is a proposal to introduce a custom invalid type or extend never to allow for such usecases.
A sensible approach could be to treat everything as immutable by default, even if it isn't.
This can sometimes lead to ugly messes though.
const pikachu : Pokemon = {
name: "Pikachu",
level: 20,
startingAbility: {
name: "Tackle",
type: {
name: "FIRE",
multiplier: 2
}
}
}
const fixedPikachu : Pokemon = {
...pikachu,
startingAbility: {
...pikachu.startingAbility,
type: {
...pikachu.startingAbility.type,
name: "NORMAL"
}
}
}
(Looks like most React code I encounter)
So a cool way of doing the same thing without mutating is using immer:
import { produce } from 'immer';
const fixedPikachu : Pokemon = produce(pikachu, (draft) => {
draft.startingAbility.type.name = "NORMAL"
});
Mutate away!
If you're familiar with Redux you most likely encountered this already.
I often need to log certain datatypes in every programming language I use while debugging.
In Rust I can use the Debug trait to achieve this:
#[derive(Debug)]
struct Pokemon {
name: String,
level: u32,
trainer: String,
}
let pikachu = Pokemon {
name: "Pikachu".to_string(),
level: 20,
trainer: "Ash".to_string()
};
println!("{:?}", pikachu);
// => Pokemon { name: "Pikachu", level: 20, trainer: "Ash" }
They can be used for all sorts of things. It's a comfortable way to define shared behaviour without inheritance.
Let's make our own debug trait in Typescript!
class Pokemon {
constructor(public name: string, public level: number, public trainer: string) {}
}
const pikachu : Pokemon = new Pokemon("Pikachu", 20, "Ash");
console.log(`${pikachu}`) // => [object Object]
This isn't very helpful :(
class Pokemon {
constructor(public name: string, public level: number, public trainer: string) {}
toString() {
return JSON.stringify(this);
}
}
const pikachu : Pokemon = new Pokemon("Pikachu", 20, "Ash");
console.log(`${pikachu}`) // => {"name":"Pikachu","level":20,"trainer":"Ash"}
Much better!
class Debug {
toString() {
return JSON.stringify(this);
}
}
class Pokemon extends Debug {
constructor(public name: string, public level: number, public trainer: string) {
super()
}
}
const pikachu : Pokemon = new Pokemon("Pikachu", 20, "Ash");
console.log(`${pikachu}`) // {"name":"Pikachu","level":20,"trainer":"Ash"}
But now we cannot inherit from anything else!
The concept for achieving this regardless of this limitations is called mixins.
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
if (name!== 'constructor') {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name)
|| Object.create(null)
);
}
});
});
}
class Debug {
toString() {
return JSON.stringify(this);
}
}
class Pokemon {
constructor(public name: string, public level: number, public trainer: string) {
applyMixins(this.constructor, [Debug]);
}
}
const pikachu : Pokemon = new Pokemon("Pikachu", 20, "Ash");
console.log(`${pikachu}`) // => {"name":"Pikachu","level":20,"trainer":"Ash"}
So Elixir is all about pipes, let's have a look:
Let's expand our pokémon example and cheat a little bit.
How about we set every pokémon of Ash to level 99.
In Typescript:
team
.filter(({trainer}) => trainer === "Ash")
.map(p => ({...p, level: 99}));
This doesn't look that different in Elixir:
team
|> Enum.filter(fn pokemon -> pokemon["trainer"] == "Ash" end)
|> Enum.map(&Map.put(&1, "level", 99))
This emphasized by the overabundance of the pipe operator |>
The pipe operator takes the value and inserts it as the first parameter into the next function.
In fact it's so common that there is a way to do this in Typescript as well using: fp-ts
const ashFilter = (pokemon: Pokemon[]) =>
pokemon.filter(({ trainer }) => trainer === "Ash")
const cheat = (pokemon: Pokemon[]) =>
pokemon.map(p => ({...p, level: 99}));
pipe(team, ashFilter, cheat) // <== right here :)
The main way to debug things in Elixir is using the IO.inspect
function which can be plugged into anywhere in your pipeline:
team
|> IO.inspect() # prints the input
|> Enum.filter(fn pokemon -> pokemon["trainer"] == "Ash" end)
|> IO.inspect() # prints the filtered input
|> Enum.map(&Map.put(&1, "level", 99))
|> IO.inspect()
# => [
# %{"level" => 99, "name" => "Pikachu", "trainer" => "Ash"},
# %{"level" => 99, "name" => "Snorlax", "trainer" => "Ash"}
# ]
const ashFilter = (pokemon: Pokemon[]) =>
pokemon.filter(({ trainer }) => trainer === "Ash")
const cheat = (pokemon: Pokemon[]) =>
pokemon.map(p => ({...p, level: 99}));
pipe(team, console.log, ashFilter, console.log, cheat, console.log)
or if you do not want to use fp-ts
I can recommend the debugger;
const filteredTeam = team.filter(({trainer}) => trainer === "Ash")
debugger; // stops the execution and let's you jump right in
const cheatedTeam = filteredTeam.map(p => ({...p, level: 99}));
debugger;
An alternative way to implement the cheat function is to use pattern matching:
# we update the level only if the pokemon belongs to Ash
def cheat(%{"trainer" => "Ash"} = pokemon) do
Map.put(pokemon, :level, 99)
end
# we do nothing otherwise
def cheat(pokemon) do
pokemon
end
team |> Enum.map(&cheat/1) # this works, no filtering required :)
There is actual an open proposal to add Pattern Matching to Typescript.
Until then we can use the already introduced ts-pattern lib
const isTrainedByAsh = (pokemon: Pokemon) => pokemon.trainer === "Ash"
const levelUp = (pokemon: Pokemon) => ({...pokemon, level: 99})
const cheat = (pokemon: Pokemon) => match(pokemon)
.with(P.when(isTrainedByAsh), levelUp)
.otherwise((pokemon) => pokemon);
team.map(cheat);
I don't hate this :)
Learning new languages shapes and forms you as a developer! You also don't want to be a one-trick pony!
Cool libraries and tools:
This presentation was implemented using marp
and uses the rose-pine-dawn theme.
I'd love to hear your thoughts, feel free to:
- ask questions now
- connect on LinkedIn
- approach me later :)