Exkurs: TypeScript für JavaScript-Entwickler
In diesem Post
Ein eigenständiger Exkurs zur Blogreihe — kein TypeScript-Referenz-Handbuch, sondern die Konzepte die du brauchst um den Code der Recipe-App lesen und schreiben zu können.
- Was TypeScript macht
- Typen schreiben
- Type Inference
- Union Types
- Typen kombinieren
- Funktionen typisieren
- Generics
- SvelteKit Typen
- Gotcha
Was TypeScript macht
TypeScript ist kein neues Framework und keine neue Sprache im eigentlichen Sinn. Es ist JavaScript mit einer zusätzlichen Schicht: Typ-Annotationen die dem Editor und dem Compiler sagen was eine Variable enthalten darf.
Der entscheidende Punkt: TypeScript existiert nur während der Entwicklung. Bevor der Code im Browser läuft, wird alles TypeScript-spezifische entfernt — was übrig bleibt ist gewöhnliches JavaScript.
// Das schreibst du:
const name: string = 'Lasagne';
// Das läuft im Browser:
const name = 'Lasagne';
TypeScript verhindert keine Bugs zur Laufzeit. Es macht Fehler sichtbar bevor der Code läuft — im Editor, während du tippst. Das ist der ganze Wert.
Typen schreiben
In JavaScript schreibst du:
const ingredient = { amount: 2, unit: 'EL', name: 'Olivenöl' };
In TypeScript kannst du beschreiben wie dieses Objekt immer aussehen soll:
type Ingredient = {
amount: number | null;
unit: string;
name: string;
};
Das ist aus src/lib/types.ts — die echte Definition aus dem Projekt. Ab jetzt weiß TypeScript: überall wo Ingredient steht, muss das Objekt genau diese drei Felder haben.
Warum number | null statt nur number?
Manche Zutaten haben keine Mengenangabe — "eine Prise Salz" hat kein sinnvolles amount. | null sagt TypeScript: dieser Wert darf fehlen. Ohne das würde TypeScript sich beschweren wenn man null zuweist.
Die Grundtypen:
let name: string = 'Lasagne';
let servings: number = 4;
let isDraft: boolean = true;
let createdAt: string = '2026-05-28'; // Datum als ISO-String
let favoriteId: string | null = null; // vorhanden oder nicht
Für Arrays:
let steps: string[] = ['Zwiebeln schneiden', 'Anbraten'];
let ingredients: Ingredient[] = [...]; // Array von Ingredient-Objekten
Type Inference
TypeScript muss nicht überall eine Typ-Annotation haben. Wenn der Wert klar ist, erkennt TypeScript den Typ automatisch:
const name = 'Lasagne'; // TypeScript weiß: string
const count = 4; // TypeScript weiß: number
const isDraft = false; // TypeScript weiß: boolean
Das nennt sich Type Inference — TypeScript denkt mit. In der Praxis schreibt man Typ-Annotationen nur dort wo TypeScript nicht selbst draufkommt:
// TypeScript braucht Hilfe — es sieht nur einen leeren Array:
const recipes: Recipe[] = [];
// TypeScript braucht keine Hilfe — der Wert ist klar:
const name = recipe.name;
Die Faustregel: Annotationen wo TypeScript nicht selbst draufkommt, Inference überall sonst.
TypeScript kann nur inferieren wenn es einen Startwert sieht. Gibt es keinen Startwert, hat TypeScript nichts woraus es den Typ ableiten kann — dann muss man selbst schreiben:
// Kein Startwert → TypeScript sieht nur "leeres Array", weiß nicht was rein soll
const recipes: Recipe[] = []; // ✅ Annotation nötig
let filter: 'published' | 'drafts'; // ✅ Annotation nötig — kein Wert zugewiesen
// Startwert vorhanden → TypeScript erkennt den Typ selbst
const name = recipe.name; // ✅ string — TypeScript weiß es aus recipe.name
const isDraft = false; // ✅ boolean — offensichtlich
const count = sessions.length; // ✅ number — .length ist immer number
Bei Funktionen gilt: den Rückgabewert kann TypeScript meistens selbst ableiten — aber die Parameter nicht, weil beim Aufruf der Funktion noch nichts bekannt ist:
// Parameter brauchen Annotation — TypeScript weiß beim Definieren nicht was übergeben wird
function getRecipeList(supabase: SupabaseClient) { ... }
// Rückgabewert kann TypeScript aus dem return-Statement ableiten — Annotation optional
// explizit: : Promise<RecipeListItem[]>
// implizit: TypeScript sieht was zurückgegeben wird und schlussfolgert selbst
Union Types
Union Types beschreiben Werte die mehrere mögliche Formen haben. In JavaScript macht man das implizit, in TypeScript explizit:
// Dieser Filter kann nur einen von vier Werten haben:
let filter: 'published' | 'drafts' | 'cooked' | 'favorites' = 'published';
Aus +page.svelte — das ist der echte Code. TypeScript weiß jetzt: filter = 'deleted' ist ein Fehler, filter = 'drafts' ist erlaubt.
Besonders häufig: string | null und number | null für optionale Datenbankfelder:
type Recipe = {
favorite_version_id: string | null; // gepinnte Version — oder keine
avg_rating: number | null; // bewertet — oder noch nicht
};
Wenn man mit einem | null-Wert arbeitet, verlangt TypeScript einen Check:
// TypeScript beschwert sich — favorite_version_id könnte null sein:
const id = recipe.favorite_version_id.toUpperCase(); // ❌
// So ist es korrekt:
if (recipe.favorite_version_id) {
const id = recipe.favorite_version_id.toUpperCase(); // ✅
}
// Oder mit dem Nullish-Operator:
const id = recipe.favorite_version_id ?? 'keine Version';
Typen kombinieren
Mit & lassen sich zwei Typen zusammenführen — das Ergebnis hat alle Felder beider Typen:
type Recipe = {
id: string;
name: string;
is_draft: boolean;
// ...
};
type RecipeListItem = Recipe & {
latest_version: { id: string; version_number: number } | null;
best_version_number: number | null;
avg_rating: number | null;
cook_count: number;
};
RecipeListItem ist alles was Recipe hat, plus vier berechnete Felder die db.ts ergänzt. Das & spart die Doppelung — man schreibt die Recipe-Felder nicht zweimal.
In JavaScript gibt es kein Äquivalent dazu — man würde einfach ein Objekt mit allen Feldern bauen und hoffen dass man nichts vergisst. TypeScript macht das zur Pflicht.
Funktionen typisieren
In JavaScript:
async function getRecipeList(supabase) {
// was ist supabase? was kommt zurück?
}
In TypeScript:
export async function getRecipeList(supabase: SupabaseClient): Promise<RecipeListItem[]> {
// supabase ist ein SupabaseClient
// die Funktion gibt ein Promise zurück das ein Array von RecipeListItem enthält
}
Das ist aus src/lib/db.ts. Der Vorteil: wenn man getRecipeList() aufruft, weiß der Editor sofort was zurückkommt — mit Autovervollständigung für alle Felder von RecipeListItem.
Parameter typisieren:
export async function createRecipe(
supabase: SupabaseClient,
params: {
name: string;
description: string;
ingredients: Ingredient[];
steps: string[];
servings: number | null;
}
) { ... }
Statt einem losen Objekt mit unbekanntem Inhalt weiß TypeScript exakt welche Felder params haben muss. Fehlt eines beim Aufruf — Fehler im Editor, nicht erst zur Laufzeit.
Generics
Generics wirken beim ersten Hinschauen einschüchternd. Das Konzept dahinter ist einfach: ein Typ der einen Platzhalter hat der erst beim Verwenden befüllt wird.
Das bekannteste Beispiel:
// Promise<T> — T ist der Platzhalter
async function getRecipes(): Promise<Recipe[]> { ... }
// ↑ hier wird T zu Recipe[]
Promise<Recipe[]> heißt: ein Promise das sich zu einem Array von Recipe auflöst. TypeScript weiß dann ohne weiteres Zutun was in .then() ankommt.
Ein weiteres Beispiel aus dem Projekt:
// Record<K, V> — ein Objekt mit Keys vom Typ K und Values vom Typ V
let favoriteOverrides: Record<string, boolean> = {};
// Key: recipe-id (string), Value: ob Favorit (boolean)
Das ist der favoriteOverrides-State auf der Home-Seite — ein Objekt das UI-seitige Favoriten-Overrides speichert bevor sie in die DB geschrieben werden.
Generics selbst schreiben muss man für dieses Projekt nicht — aber lesen können ist wichtig weil SvelteKit sie überall verwendet.
SvelteKit Typen
SvelteKit generiert beim Build automatisch Typen für jede Route. Man muss sie nicht schreiben — man importiert sie:
// +page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals: { supabase } }) => {
return { recipes: await getRecipeList(supabase) };
};
PageServerLoad ist ein von SvelteKit generierter Typ der genau beschreibt was die load-Funktion dieser Route bekommen und zurückgeben darf.
<!-- +page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
PageData ist der passende Gegen-Typ: er enthält exakt was der Server Load zurückgibt — hier also { recipes: RecipeListItem[] }. TypeScript weiß damit in der Svelte-Komponente was data.recipes enthält, ohne dass man es manuell definieren muss.
app.d.ts — globale Typ-Deklarationen:
declare global {
namespace App {
interface Locals {
supabase: SupabaseClient;
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>;
}
}
}
Diese Datei erweitert SvelteKit's globale Typen. Ohne sie würde TypeScript bei locals.supabase sagen: Eigenschaft 'supabase' existiert nicht auf Typ 'Locals'. Mit ihr ist locals.supabase überall korrekt typisiert — in hooks.server.ts, in Load-Funktionen, überall.
Gotcha
TypeScript ist so gut wie die Typen die man schreibt. Zwei Stellen wo es still bleibt obwohl etwas nicht stimmt:
any — der Notausgang:
const data: any = supabase.from('recipes').select('*');
// TypeScript: alles erlaubt, keine Warnungen
// du: weißt nicht was drinsteckt
any schaltet TypeScript für diesen Wert ab. Im Projekt wird es vermieden — stattdessen gibt es types.ts mit expliziten Typen. Wenn du any siehst, ist es ein Hinweis dass dort noch Arbeit offen ist.
Fehlende null-Checks:
const version = recipe.latest_version; // RecipeVersion | null
console.log(version.version_number); // TypeScript warnt — ✅
TypeScript warnt hier korrekt. Aber manchmal schreibt man:
const version = recipe.latest_version!; // das ! sagt: "ich weiß es ist nicht null"
Das ! ist ein Versprechen an TypeScript — und wenn das Versprechen falsch ist, crasht die App zur Laufzeit ohne Vorwarnung. Im Projekt wird es sparsam eingesetzt, nur wo man es wirklich weiß.
Zusammenfassung
TypeScript ist kein Selbstzweck — es macht den Code lesbarer und Fehler sichtbarer bevor sie passieren. Die Konzepte die du für dieses Projekt brauchst:
type— beschreibt die Form eines Objektsstring | null— Union Types für optionale WerteType & { ... }— Typen zusammenführen- Funktions-Signaturen — was rein geht und was rauskommt
Promise<T>— was ein async-Aufruf zurückgibtPageData,PageServerLoad— SvelteKit generiert diese, du importierst sie
Der Rest — komplexe Generics, Conditional Types, Decorators — ist für dieses Projekt nicht nötig.
Dieser Post ist ein Exkurs zur Blogreihe über den Bau einer Rezept-App mit SvelteKit 5 und Supabase. Weiter geht es in Teil 2: Svelte 5 Runes — reaktiver State von Grund auf.