diff --git a/05_typescript/docs/in-operator-vs-type-predicate.pdf b/05_typescript/docs/in-operator-vs-type-predicate.pdf new file mode 100644 index 0000000..57c2923 Binary files /dev/null and b/05_typescript/docs/in-operator-vs-type-predicate.pdf differ diff --git a/05_typescript/unterricht/tag31/01_ts-narrowing-guards/assets/js/04_in-operator.js b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/assets/js/04_in-operator.js index 1cd87df..873a7ab 100644 --- a/05_typescript/unterricht/tag31/01_ts-narrowing-guards/assets/js/04_in-operator.js +++ b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/assets/js/04_in-operator.js @@ -1,5 +1,30 @@ (() => { // src/04_in-operator.ts { + const operate = (vehicle) => { + if ("drive" in vehicle) { + vehicle.drive(); + } else { + vehicle.sail(); + } + }; + const car = { drive: () => console.log("I'm driving") }; + const sealander = { sail: () => console.log("I'm sailing") }; + operate(car); + operate(sealander); + const move = (entity) => { + if ("walk" in entity) { + entity.walk?.(); + } else if ("fly" in entity && entity.fly) { + entity.fly(); + } else if ("bark" in entity) { + entity.bark(); + } + }; + const dog = { bark: () => console.log("Woof") }; + const bird = { fly: () => console.log("Flying") }; + const robot = { walk: () => console.log("Walking") }; + const optimus = { fly: () => console.log("Flying Robo") }; + move(robot); } })(); diff --git a/05_typescript/unterricht/tag31/01_ts-narrowing-guards/assets/js/05_kontrollflussanalyse.js b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/assets/js/05_kontrollflussanalyse.js new file mode 100644 index 0000000..9428cf2 --- /dev/null +++ b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/assets/js/05_kontrollflussanalyse.js @@ -0,0 +1,37 @@ +(() => { + // src/05_kontrollflussanalyse.ts + { + const processData = (data) => { + if (typeof data === "boolean") { + console.log("Boolean value:", data); + } else if (typeof data === "string") { + console.log("String value:", data.toUpperCase()); + } else { + console.log("Number value:", data.toFixed(2)); + } + }; + processData(true); + processData("hello"); + processData(123); + const checkValue = (value) => { + if (typeof value === "string") { + return `String: ${value}`; + } + return `Number: ${value.toFixed(2)}`; + }; + console.log(checkValue("hello")); + console.log(checkValue(123)); + const describeInput = (input) => { + if (input === null) { + return "No input provided."; + } + if (typeof input === "string") { + return `String input: ${input.toUpperCase()}`; + } + return `Number input: ${input.toFixed(2)}`; + }; + console.log(describeInput("hello")); + console.log(describeInput(123)); + console.log(describeInput(null)); + } +})(); diff --git a/05_typescript/unterricht/tag31/01_ts-narrowing-guards/assets/js/06_type-predicate.js b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/assets/js/06_type-predicate.js new file mode 100644 index 0000000..f995964 --- /dev/null +++ b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/assets/js/06_type-predicate.js @@ -0,0 +1,28 @@ +(() => { + // src/06_type-predicate.ts + { + const isFish = (pet2) => { + return Object.hasOwn(pet2, "swim"); + }; + const handlePet = (pet2) => { + if (isFish(pet2)) { + pet2.swim(); + } else { + pet2.fly(); + } + }; + const pet = { swim: () => console.log("Is swimming") }; + const amsel = { fly: () => console.log("Is flying") }; + handlePet(pet); + handlePet(amsel); + const zoo = [ + { swim: () => console.log("Fish swimming") }, + { fly: () => console.log("Bird flying") }, + { fly: () => console.log("Another Bird flying") }, + { fly: () => console.log("Bird3 flying") }, + { swim: () => console.log("Another Fish flying") } + ]; + const fishes = zoo.filter((pet2) => isFish(pet2)); + fishes.forEach((fish) => fish.swim()); + } +})(); diff --git a/05_typescript/unterricht/tag31/01_ts-narrowing-guards/index.html b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/index.html index e99f67d..d28f648 100644 --- a/05_typescript/unterricht/tag31/01_ts-narrowing-guards/index.html +++ b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/index.html @@ -6,9 +6,12 @@ TypeScript - Grundlagen - + +
diff --git a/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/04_in-operator.md b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/04_in-operator.md new file mode 100644 index 0000000..71a8048 --- /dev/null +++ b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/04_in-operator.md @@ -0,0 +1,86 @@ +## `in`-Operator vs Type Predicate + +Der `in`-Operator fungiert in TypeScript als **eingebauter Type Guard** und bewirkt ein automatisches **Type Narrowing**, verhält sich im Detail jedoch flexibler und nachsichtiger als ein handgeschriebenes Type Predicate (`pet is Fish`). + +--- + +### Gemeinsamkeit + +Beide Mechanismen verengen eine Union: + +```typescript +// Variante A: Direkt mit `in` +if ('swim' in pet) { + pet.swim(); // TS verengt pet zu Fish +} else { + pet.fly(); // TS verengt pet zu Bird +} + +// Variante B: Mit Type Predicate +if (isFish(pet)) { + pet.swim(); // TS verengt pet zu Fish +} else { + pet.fly(); // TS verengt pet zu Bird +} + +``` + +In einfachen Unions mit distinkten Properties ist der praktische Effekt im `if`/`else`-Block identisch. + +--- + +### Wesentliche Unterschiede + +| Kriterium | `in`-Operator | Eigene Predicate-Funktion (`pet is Fish`) | +| --- | --- | --- | +| **Prüflogik** | Prüft ausschließlich die **Existenz des Keys** (auch in der Prototypenkette). | Beliebige Laufzeit-Validierung (Werte, Typen, verschachtelte Felder, Regex etc.). | +| **Wiederverwendbarkeit** | Nur inline an der Stelle der Prüfung. | Modular exportierbar und z. B. in `.filter(isFish)` nutzbar. | +| **Typsicherheit** | **Vollständig vom Compiler verifiziert.** TS schaut sich die Typdefinitionen an. | **Verhält sich wie ein Type Cast (`as`).** TS vertraut blind dem Rückgabewert `pet is Fish`, selbst wenn die Laufzeitprüfung fehlerhaft ist. | +| **Umgang mit unbekannten Typen** | Verengt `object` oder `unknown` zu `Record<"swim", unknown> & object`. | Verengt direkt auf den Zieltyp `Fish`. | + +--- + +### 1. Type Predicates sind "Vertrauenssache" (Compiler prüft nicht den Inhalt) + +Wenn du ein Prädikat schreibst: + +```typescript +const isFish = (pet: Fish | Bird): pet is Fish => { + return true; // Bug! +}; + +if (isFish(pet)) { + pet.swim(); // TS meckert nicht, crasht aber zur Laufzeit +} + +``` + +TypeScript überprüft nicht, ob die Logik im Body von `isFish` tatsächlich beweist, dass es sich um einen Fisch handelt. Beim `in`-Operator hingegen wertet TypeScript die tatsächlichen Eigenschaften der beteiligten Typen aus. + +--- + +### 2. Wiederverwendbarkeit in Array-Methoden + +Ein großer Vorteil des Type Predicates ist die Verwendung in Higher-Order Functions: + +```typescript +const pets: (Fish | Bird)[] = [/* ... */]; + +// Funktioniert nur mit Predicate: +const fishes: Fish[] = pets.filter(isFish); + +// Funktioniert nicht direkt mit 'in': +// pets.filter(pet => 'swim' in pet) liefert weiterhin (Fish | Bird)[] + +``` + +--- + +### 3. Prototypenkette vs. Own Properties + +In deinem Code-Kommentar steht: + +* `Object.hasOwn(pet, 'swim')` bzw. `pet.hasOwnProperty('swim')` prüft nur **eigene Eigenschaften**. +* `'swim' in pet` prüft auch Methoden, die auf dem **Prototyp** definiert sind (z. B. wenn `Fish` eine Klasse ist). + +Wenn `Fish` als Klasse deklariert ist, liegt `swim()` auf `Fish.prototype`. In diesem Fall würde `Object.hasOwn(pet, 'swim')` zur Laufzeit `false` liefern, während `'swim' in pet` korrekt `true` ergibt. \ No newline at end of file diff --git a/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/04_in-operator.ts b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/04_in-operator.ts index 92a7231..45bfd6f 100644 --- a/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/04_in-operator.ts +++ b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/04_in-operator.ts @@ -1,5 +1,55 @@ // In Operator - wird verwendet, um zu bestimmen, ob ein Objekt ein bestimmtes Property hat. { - type Car = { drive: () => void }; - type Boat = { sail: () => void }; + type Car = { + drive: () => void; + // drive():void + }; + type Boat = { + sail: () => void; + // sail(): void + }; + + const operate = (vehicle: Car | Boat) => { + // Property 'drive' does not exist on type 'Boat'. + // vehicle.drive(); // Property 'drive' does not exist on type 'Car | Boat'. + + // Object.hasOwn() führt kein narrowing aus | grenzt nicht den Datentyp ein + // if (Object.hasOwn(vehicle, 'drive')) { + if ('drive' in vehicle) { + vehicle.drive(); + } else { + vehicle.sail(); + } + }; + + const car: Car = { drive: () => console.log("I'm driving") }; + const sealander: Boat = { sail: () => console.log("I'm sailing") }; + + operate(car); + operate(sealander); + + // ------- + type Dog = { bark: () => void }; + type Bird = { fly: () => void }; + type Robot = { walk?: () => void; fly?: () => void | undefined }; + + const move = (entity: Dog | Bird | Robot) => { + if ('walk' in entity) { + entity.walk?.(); + } else if ('fly' in entity && entity.fly) { + entity.fly(); + } else if ('bark' in entity) { + entity.bark(); + } + }; + + const dog: Dog = { bark: () => console.log('Woof') }; + const bird: Bird = { fly: () => console.log('Flying') }; + const robot: Robot = { walk: () => console.log('Walking') }; + const optimus: Robot = { fly: () => console.log('Flying Robo') }; + + // move(dog); + // move(bird); + move(robot); + // move(optimus); } diff --git a/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/05_kontrollflussanalyse.ts b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/05_kontrollflussanalyse.ts new file mode 100644 index 0000000..66b4791 --- /dev/null +++ b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/05_kontrollflussanalyse.ts @@ -0,0 +1,52 @@ +// Kontrollflussanalyse +/* +In TypeScript wird der Kontrollfluss analysiert, um zu bestimmen, welche Typen eine Variable haben kann. Dies wird auch als "Type Narrowing" bezeichnet. +Type Narrowing wird verwendet, um den Typ einer Variablen zu beschränken, wenn bestimmte Bedingungen erfüllt sind. +*/ + +{ + const processData = (data: string | number | boolean): void => { + if (typeof data === 'boolean') { + console.log('Boolean value:', data); + } else if (typeof data === 'string') { + console.log('String value:', data.toUpperCase()); + } else { + console.log('Number value:', data.toFixed(2)); + } + }; + + processData(true); + processData('hello'); + processData(123); + + // Kontrollfluss mit Early Returns + + const checkValue = (value: string | number): string => { + // Guard + if (typeof value === 'string') { + return `String: ${value}`; // early return bei Narrowing + } + + return `Number: ${value.toFixed(2)}`; + }; + + console.log(checkValue('hello')); + console.log(checkValue(123)); + + // --- + const describeInput = (input: string | number | null): string => { + if (input === null) { + return 'No input provided.'; + } + + if (typeof input === 'string') { + return `String input: ${input.toUpperCase()}`; + } + + return `Number input: ${input.toFixed(2)}`; + }; + + console.log(describeInput('hello')); + console.log(describeInput(123)); + console.log(describeInput(null)); +} diff --git a/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/06_type-predicate.ts b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/06_type-predicate.ts new file mode 100644 index 0000000..0740dd7 --- /dev/null +++ b/05_typescript/unterricht/tag31/01_ts-narrowing-guards/src/06_type-predicate.ts @@ -0,0 +1,57 @@ +// Type Predicate +/* +Ein Type Predicate ist eine besondere Art von Funktion, die einen booleschen Wert zurückgibt, der angibt, ob eine Variable einem bestimmten Typ entspricht. + +WICHTIG: "Außerdem grenzt es den Typ der Variablen auf der Grundlage des Ergebnisses der Prüfung ein." +*/ + +{ + type Fish = { swim: () => void }; + type Bird = { fly: () => void }; + + // "pet is Fish" <- Type predicate + const isFish = (pet: Fish | Bird): pet is Fish => { + return Object.hasOwn(pet, 'swim'); + // return pet.hasOwnProperty('swim'); + // return typeof (pet as Fish).swim !== 'undefined'; + + // return 'swim' in pet; // in Operator führt Type-Narrowing durch + }; + + const handlePet = (pet: Fish | Bird) => { + // Property 'swim' does not exist on type 'Fish | Bird'. + // Property 'swim' does not exist on type 'Bird'. + // pet.swim(); + + // if ('swim' in pet) { + if (isFish(pet)) { + pet.swim(); // TypeScript knows pet is a Fish Object + } else { + pet.fly(); // TypeScript knows pet is a Bird Object + } + }; + + const pet: Fish | Bird = { swim: () => console.log('Is swimming') }; + const amsel: Bird = { fly: () => console.log('Is flying') }; + + handlePet(pet); + handlePet(amsel); + + //------ + // const zoo: (Fish | Bird)[] = [ + // { swim: () => console.log('Fish swimming') }, + // { fly: () => console.log('Bird flying') }, + // { swim: () => console.log('Another Fish flying') }, + // ]; + const zoo: Array = [ + { swim: () => console.log('Fish swimming') }, + { fly: () => console.log('Bird flying') }, + { fly: () => console.log('Another Bird flying') }, + { fly: () => console.log('Bird3 flying') }, + { swim: () => console.log('Another Fish flying') }, + ]; + + const fishes = zoo.filter((pet) => isFish(pet)); + + fishes.forEach((fish) => fish.swim()); +}