🕵️ SicherheitslückenWhat continuous operational resilience looks like under DORA(09.09.2026 um 17:53 Uhr)
🔧 AI Nachrichten OpenAI seeks tougher AI rules. CIOs may feel the ripple effects(10.09.2026 um 12:11 Uhr)
🔧 AI Nachrichten Mistral valued at €21bn after €3bn Series D funding round(08.09.2026 um 10:19 Uhr)
🪟 Windows TippsWindows XP's Cursor Indicator Is Getting a Windows 11 Refresh(25.08.2026 um 13:00 Uhr)
🕵️ SicherheitslückenWhat continuous operational resilience looks like under DORA(09.09.2026 um 17:53 Uhr)
🔧 AI Nachrichten OpenAI seeks tougher AI rules. CIOs may feel the ripple effects(10.09.2026 um 12:11 Uhr)
🔧 AI Nachrichten Mistral valued at €21bn after €3bn Series D funding round(08.09.2026 um 10:19 Uhr)
🪟 Windows TippsWindows XP's Cursor Indicator Is Getting a Windows 11 Refresh(25.08.2026 um 13:00 Uhr)

🔧 Programmierung 🕛 vor 5 Monaten 12 Min Lesezeit
0

The Type System: What You Know, What's New, and What's Weird

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht


  • My project:


  • Me: , we covered the big mental shifts: structural typing, type erasure, null vs undefined, how overloading isn't really overloading. That was the "prepare yourself" post. This one is where we actually build things with the type system.



    I'll split it by feel: the stuff that'll be instantly familiar, the stuff that's genuinely new, and the stuff that'll trip you up because it looks familiar but behaves differently.






    Primitives, Arrays, Objects: The Familiar Stuff



    I'll keep this short because you already know what types are.




    CODE
    const name: string = "Gabriel";
    const age: number = 31;
    const isActive: boolean = true;






    No int vs float vs double. It's all number. There's also bigint if you need arbitrary precision, but number covers 99% of cases.



    Arrays have two syntaxes:




    CODE
    const ids: number[] = [1, 2, 3];
    const names: Array<string> = ["Alice", "Bob"];






    Both do the same thing. I use number[] because it's shorter. Some teams prefer Array<string> for consistency with other generic types. Pick one, move on.



    Object types look like this:




    CODE
    const user: { id: number; name: string; email: string } = {
    id: 1,
    name: "Gabriel",
    email: "[email protected]",
    };






    You wouldn't actually inline that type everywhere. You'd extract it. Which brings us to the first real decision you'll face.






    Type Aliases vs Interfaces



    In Java or C#, you have classes and interfaces. In TypeScript, you have type and interface for describing the shape of data, and they overlap a lot.




    CODE
    // Type alias
    type User = {
    id: number;
    name: string;
    email: string;
    };

    // Interface
    interface User {
    id: number;
    name: string;
    email: string;
    }






    For object shapes, these are nearly interchangeable. Both support extending:




    CODE
    // Type uses intersection
    type AdminUser = User & {
    permissions: string[];
    };

    // Interface uses extends
    interface AdminUser extends User {
    permissions: string[];
    }






    So which one do you pick?



    Here's my take: use type for almost everything. Use interface when you specifically need declaration merging (where multiple interface declarations with the same name combine automatically) or when you're designing a public API that other packages will extend.



    Declaration merging is a real thing:




    CODE
    interface Window {
    myCustomProperty: string;
    }
    // This merges with the existing Window interface
    // rather than overwriting it






    You can't do that with type. But how often do you actually need that? Almost never in application code. type aliases are more flexible: they can represent unions, intersections, primitives, tuples. Interfaces can only describe object shapes.




    CODE
    type ID = string | number;          // Can't do this with an interface
    type Pair = [string, number]; // Can't do this either
    type Callback = (data: string) => void; // Or this






    The TypeScript team has gone back and forth on recommendations over the years. My rule: type by default, interface when you have a specific reason.






    Union Types: This Changes Everything



    If you come from Java, you've probably written something like this to handle a value that could be one of several types:




    CODE
    // Java: the clunky way
    public Object parseInput(String raw) {
    try {
    return Integer.parseInt(raw);
    } catch (NumberFormatException e) {
    return raw;
    }
    }
    // Now you're stuck with Object and casting everywhere






    TypeScript unions solve this directly:




    CODE
    function parseInput(raw: string): number | string {
    const parsed = Number(raw);
    return Number.isNaN(parsed) ? raw : parsed;
    }

    const result = parseInput("42");
    // result is number | string






    The compiler tracks the union. You can narrow it with typeof:




    CODE
    if (typeof result === "number") {
    // TypeScript knows result is number here
    console.log(result.toFixed(2));
    } else {
    // TypeScript knows result is string here
    console.log(result.toUpperCase());
    }






    This is called type narrowing, and it's one of the most powerful features in the type system. No casts, no instanceof chains with abstract base classes. The compiler follows your control flow and narrows the type automatically.



    A more realistic version, modeling API responses:




    CODE
    type ApiResponse<T> =
    | { success: true; data: T }
    | { success: false; error: string };

    function handleResponse(response: ApiResponse<User>) {
    if (response.success) {
    // TypeScript knows response.data exists here
    console.log(response.data.name);
    } else {
    // TypeScript knows response.error exists here
    console.log(response.error);
    }
    }






    In Java, you'd model this with a sealed interface or a Result<T> class with subclasses. In Kotlin, you'd use a sealed class. TypeScript does it with plain objects and unions. No class hierarchy needed.






    Literal Types and as const



    In Java, if you want a type that can only be one of a few string values, you reach for an enum. In TypeScript, you often don't need to.




    CODE
    type OrderStatus = "pending" | "processing" | "shipped" | "delivered" | "cancelled";

    function updateOrder(orderId: string, status: OrderStatus) {
    // ...
    }

    updateOrder("abc-123", "shipped"); // works
    updateOrder("abc-123", "exploded"); // compile error






    That's it. No class, no enum declaration. Just a union of string literals. The compiler enforces it.



    You can do the same with numbers:




    CODE
    type HttpSuccessCode = 200 | 201 | 204;
    type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;






    Now, as const. This one confused me for a while. When you write:




    CODE
    const config = {
    apiUrl: "https://api.example.com",
    timeout: 5000,
    retries: 3,
    };
    // TypeScript infers: { apiUrl: string; timeout: number; retries: number }






    TypeScript widens the types. apiUrl is string, not "https://api.example.com". That's usually fine. But sometimes you want the literal types preserved:




    CODE
    const config = {
    apiUrl: "https://api.example.com",
    timeout: 5000,
    retries: 3,
    } as const;
    // Now it's: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000; readonly retries: 3 }






    as const does two things: makes everything readonly and preserves literal types. It's especially useful with arrays:




    CODE
    const ROLES = ["admin", "editor", "viewer"] as const;
    // Type is: readonly ["admin", "editor", "viewer"]

    type Role = (typeof ROLES)[number];
    // Type is: "admin" | "editor" | "viewer"






    That (typeof ROLES)[number] syntax looks strange the first time. It's indexing the tuple type with number to extract the union of all element types. You get used to it.






    Enums: The Controversial One



    TypeScript has enums. I'm going to tell you to avoid them.




    CODE
    // TypeScript enum
    enum Direction {
    Up,
    Down,
    Left,
    Right,
    }






    This looks like a Java or C# enum. The problem is what happens at compile time. Remember how I said TypeScript types get erased at runtime? Enums are the exception. They emit actual JavaScript code:




    CODE
    // Compiled output
    var Direction;
    (function (Direction) {
    Direction[(Direction["Up"] = 0)] = "Up";
    Direction[(Direction["Down"] = 1)] = "Down";
    Direction[(Direction["Left"] = 2)] = "Left";
    Direction[(Direction["Right"] = 3)] = "Right";
    })(Direction || (Direction = {}));






    That's a runtime object with bidirectional mapping. Direction.Up is 0, but Direction[0] is "Up". This creates subtle bugs. You can pass any number where a Direction is expected and the compiler won't complain:




    CODE
    function move(direction: Direction) { /* ... */ }
    move(42); // No error! This compiles fine with numeric enums.






    String enums are slightly better:




    CODE
    enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
    }






    At least now you can't pass arbitrary numbers. But you still get the runtime code emission, and you've created a nominal type that only accepts values from that specific enum, not matching string literals. move("UP") won't compile, even though the underlying value is "UP".



    What I use instead:




    CODE
    const Direction = {
    Up: "UP",
    Down: "DOWN",
    Left: "LEFT",
    Right: "RIGHT",
    } as const;

    type Direction = (typeof Direction)[keyof typeof Direction];
    // type Direction = "UP" | "DOWN" | "LEFT" | "RIGHT"






    Yes, the type and const have the same name. TypeScript allows this because types and values live in separate namespaces. You get:




    • A runtime object you can reference (Direction.Up)

    • A type you can use in annotations (direction: Direction)

    • No weird runtime code generation

    • Regular string literal unions under the hood



    The two-line pattern looks a bit unusual at first. After a week you stop noticing.






    Discriminated Unions — The Pattern That Replaces Inheritance



    This is the single most important pattern in TypeScript. If you only take one thing from this post, make it this.



    In Java, when you have a family of related types with different data, you reach for inheritance:




    CODE
    // Java approach
    sealed interface Shape permits Circle, Rectangle, Triangle {}
    record Circle(double radius) implements Shape {}
    record Rectangle(double width, double height) implements Shape {}
    record Triangle(double base, double height) implements Shape {}

    // Then pattern matching (Java 21+)
    double area(Shape shape) {
    return switch (shape) {
    case Circle c -> Math.PI * c.radius() * c.radius();
    case Rectangle r -> r.width() * r.height();
    case Triangle t -> 0.5 * t.base() * t.height();
    };
    }






    In TypeScript, you do this with a discriminated union. Each variant has a common property (the "discriminant") that tells you which variant you're dealing with:




    CODE
    type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "rectangle"; width: number; height: number }
    | { kind: "triangle"; base: number; height: number };

    function area(shape: Shape): number {
    switch (shape.kind) {
    case "circle":
    // TypeScript narrows: shape is { kind: "circle"; radius: number }
    return Math.PI * shape.radius ** 2;
    case "rectangle":
    // TypeScript narrows: shape is { kind: "rectangle"; width: number; height: number }
    return shape.width * shape.height;
    case "triangle":
    // TypeScript narrows: shape is { kind: "triangle"; base: number; height: number }
    return 0.5 * shape.base * shape.height;
    }
    }






    No classes. No new. No instanceof. Just plain objects with a tag field, and the compiler tracks which properties exist based on that tag.



    A real-world example -- handling different types of payment events:




    CODE
    type PaymentEvent =
    | { type: "payment_initiated"; orderId: string; amount: number; currency: string }
    | { type: "payment_authorized"; orderId: string; authorizationCode: string }
    | { type: "payment_captured"; orderId: string; capturedAmount: number }
    | { type: "payment_failed"; orderId: string; reason: string; retryable: boolean };

    function processPaymentEvent(event: PaymentEvent): void {
    switch (event.type) {
    case "payment_initiated":
    console.log(`Order ${event.orderId}: $${event.amount} ${event.currency}`);
    break;
    case "payment_authorized":
    console.log(`Order ${event.orderId}: authorized (${event.authorizationCode})`);
    break;
    case "payment_captured":
    console.log(`Order ${event.orderId}: captured $${event.capturedAmount}`);
    break;
    case "payment_failed":
    console.log(`Order ${event.orderId}: failed — ${event.reason}`);
    if (event.retryable) {
    // schedule retry
    }
    break;
    }
    }






    Each case narrows the type, so event.authorizationCode is only accessible in the "payment_authorized" branch. Try to access it elsewhere and you get a compile error.



    The best part: exhaustiveness checking. Add a new event type to the union and forget to handle it? The compiler will tell you.




    CODE
    function assertNever(value: never): never {
    throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
    }

    function processPaymentEvent(event: PaymentEvent): void {
    switch (event.type) {
    case "payment_initiated":
    // ...
    break;
    case "payment_authorized":
    // ...
    break;
    // Oops, forgot payment_captured and payment_failed
    default:
    assertNever(event);
    // Compile error! event is PaymentEvent, not never
    }
    }






    If you handle all cases, event in the default branch is never (an impossible type, nothing can reach there). If you miss a case, the type isn't never and assertNever refuses to accept it. The compiler catches the gap.



    This is TypeScript's answer to sealed classes and pattern matching. It's lighter-weight, it works with plain data, and it serializes/deserializes to JSON without any ceremony -- which matters a lot when you're building APIs.



    I spent my first few months writing TypeScript classes with inheritance. Once I understood discriminated unions, I deleted most of them.






    unknown vs any: Pick the Right Escape Hatch



    Coming from PHP's mixed or Java's Object, you'll be tempted to reach for any when you don't know a type.




    CODE
    function processData(data: any) {
    // No errors anywhere. TypeScript stops checking.
    console.log(data.foo.bar.baz.whatever);
    data.nonExistentMethod();
    // All compiles fine. All blows up at runtime.
    }






    any disables type checking. Not just for that variable, but for everything it touches. It's viral. If you pass an any value into a well-typed function, the return value often becomes any too.



    unknown is the type-safe alternative:




    CODE
    function processData(data: unknown) {
    // console.log(data.foo); // Compile error! Can't access properties on unknown

    // You have to narrow first
    if (typeof data === "object" && data !== null && "foo" in data) {
    console.log(data.foo);
    }
    }






    unknown means "I don't know what this is, but I'll check before I use it." That's the correct mental model for untyped external data: API responses, parsed JSON, user input, third-party library returns.



    Here's a practical example with API calls:




    CODE
    async function fetchUser(id: string): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    const data: unknown = await response.json();

    // Validate the shape before trusting it
    if (
    typeof data === "object" &&
    data !== null &&
    "id" in data &&
    "name" in data &&
    "email" in data
    ) {
    return data as User;
    }

    throw new Error("Invalid user data from API");
    }






    In practice, you'd use a validation library like Zod or Valibot instead of manual checks. We'll get into that later in the series. The point is: unknown forces you to validate, any lets you pretend everything is fine.



    My rule: never use any in application code. If you see it in a code review, push back. The one exception is type assertions in test files where fighting the type system adds no value. Even then, unknown with a cast is usually better.



    With strict: true (enabled by default when you run tsc --init in TypeScript 6), the compiler already forbids implicit any in most places. Lean into that. If you find yourself wanting to type something as any, it's a sign you need to think harder about what the actual type is.






    What's Next



    We've covered the core of TypeScript's type system: how unions, literal types, and discriminated unions replace patterns you'd normally build with class hierarchies and inheritance. There's more depth to each of these, especially once generics get involved.



    That's exactly what Post 3 is about: functions and generics. How TypeScript's generic system compares to Java's (spoiler: it's more flexible and more confusing), type inference, generic constraints, and the patterns you'll actually use in backend code.






    What's your take on enums vs as const? If you've worked in both TypeScript and a language like Java or C#, I'd like to hear which patterns you kept and which you dropped. Let me know in the comments.



    I'm building . A star helps a lot. You can follow my work at gabrielanhaia.

    Vollständiger Original-Bericht
    Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
    ↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Sam Altman calls GPT-6 Astra rollout ‘messy’ as enterprise users wait for access
1 Quelle
Swiss government explores replacing Microsoft 365 with open-source software
1 Quelle
What continuous operational resilience looks like under DORA
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten The Type System: What You Know, What's New, and What's Weird

Thematisch verwandte Begriffe: Type, System, What, Know · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...