Apptiva Logo

Typescript mit Effect

Effect ist eine Typescript-Bibliothek, die komplizierte Dinge vereinfacht: Orchestrierung von Services, Concurrency, Fehlerbehandlung, Testen und vieles mehr. Aber was ist Effect wirklich und wie wird es eingesetzt?

Publiziert am von Philip Schönholzer

Was ist Effect? Das ist eine schwierige Frage, die ich mit ein paar Beispielen beantworten möchte.

Man kann sich `Effect` vereinfacht als eine JS-Funktion mit Superpower vorstellen.

import { Effect } from 'effect'

// Dies entspricht vereinfacht etwa
// const program = () => 42
const program = Effect.succeed(42)

// Effects müssen ausgeführt werden, um den Wert zu erhalten
const result = Effect.runSync(program)

console.log('Hello World', result)

Das obige Beispiel hat aber noch keinen Vorteil gegenüber einer einfachen Funktion.

Die interessanten Fähigkeiten von `Effect` sind die Fehlerbehandlung und wie Services implementiert werden. Dies wird auch im Typ von `Effect` sichtbar.

import { Effect } from 'effect'

// Der Type von program ist dieser:
// Effect.Effect<number, NumberNotFound, NumberService>
const program = NumberService.getNumber

Dieses Programm kann den Fehler `NumberNotFound` werfen und benötigt den Service `NumberService`. Den Fehler muss ich nicht behandeln, wenn ich das nicht will, aber den Service muss ich dem Programm zur Verfügung stellen, sonst läuft es nicht (siehe Kapitel Services).

Fehlerbehandlung

Wenn ein Fehler auftritt, gibt es viele Möglichkeiten, wie ich damit umgehen kann. Entweder ich versuche den Effekt erneut auszuführen, ich stelle einen Default zur Verfügung, ich zeige den Fehler dem Benutzer an, etc.

// Effect.Effect<number, never, NumberService>
const program = NumberService.getNumber().pipe(
// Bei Fehler nochmals versuchen
Effect.retry({ times: 1 }),
// und sonst 7 als default nutzen
Effect.orElseSucceed(() => 7),
)

In diesem Beispiel wird einmal versucht, die Nummer erneut zu erhalten und ansonsten eine 7 zurückgegeben. Im `Effect` Typ ist der mittlere Typ nun auch `never`, da immer eine Nummer zurückgegeben wird.

Services

Im obigen Beispiel ist ein `NumberService` zu sehen, der den Effekt `getNumber` zur Verfügung stellt. Die Implementierung dieses Services wird erst vor der Ausführung des Programms zur Verfügung gestellt. Dadurch können je nach Situation (z.B. Produktions- oder Testumgebung) unterschiedliche Implementierungen zur Verfügung gestellt werden.

Der oben genannte Service könnte z.B. wie folgt implementiert werden:

// service.ts

import { Effect, Layer } from 'effect'

// Implementation
const make = {
getNumber: Effect.tryPromise(() => new Promise<number>((resolve) => setTimeout(() => resolve(42), 1000))),
plusOne: (a: number) => Effect.sync(a + 1),
}

// Eindeutigen Service mit dem Typ
export class NumberService extends Effect.Tag('@services/Number')<
NumberService,
typeof make
>() {
// Implementation "exportieren". Kann aber auch als Funktion
// geschehen. Hat nichts direkt mit der Klasse zu tun.
static Live = Layer.succeed(this, make)
}

Um diesen Service dem Programm zur Verfügung zu stellen, muss er noch "bereitgestellt" werden.

import { Console, Effect, Layer } from 'effect'
import { NumberService } from './service'

// Braucht den Service 👇
// Effect.Effect<number, never, NumberService>
const program = NumberService.getNumber

// Service zur Verfügung stellen (bereitstellen)
// Braucht keine weiteren Service 👇
// Effect.Effect<number, never, never>
const runnable = Effect.provide(program, NumberService.Live)

// Effect ausführen
Effect.runPromise(runnable)

Mit `Effect` ist es üblich, einem Service einen anderen Service zur Verfügung zu stellen und die Implementierung aller Services erst vor der Ausführung zur Verfügung zu stellen. So kann z.B. die DB einer Anwendung im Testfall ein Mock sein und die anderen Services, die die DB verwenden, werden normal genutzt.

Effects kombinieren

Ein Effect ist ein Legostein. Richtig cool wird es aber erst, wenn man mehrere dieser Legosteine zusammensetzen kann. Dafür gibt es verschiedene Kombinatoren und Techniken. Hier eine kleine Auswahl:

import { Console, Effect, pipe } from 'effect'

// Als Pipe kombinieren
const program1 = pipe(
Console.log('Starting program...'),
Effect.flatMap(() => Effect.succeed(42)),
Effect.map((n) => n + 1),
Effect.map((n) => `The answer is ${n}`),
)

// das gleich als Generator
const program2 = Effect.gen(function* () {
yield* Console.log('Starting program...')
const number = yield* Effect.succeed(42)
const addedUpNumber = number + 1
return `The answer is ${addedUpNumber}`
})

Infos zu Effect

Effect lernt man nicht an einem Nachmittag. Aber mit wenig Wissen kann man viel aus der Bibliothek herausholen. Mehr Infos gibt es hier:

Apptiva Logo
Apptiva AG
Eichweid 1
6203 Sempach Station

041 322 26 26

info@apptiva.ch

WhatsApp LogoChat auf WhatsApp

Newsletter

Quartalsweise Apptiva-News mit hilfreichen Insights in deinem Postfach.

Socials

Swiss Made Software

Finde heraus, was noch auf dich wartet:

Fokusthemen

Angebot

Workshops

Apptiva

Rechtliches