--- theme: gaia _class: lead paginate: true backgroundColor: #00437a color: #fff --- # **Typescript** ![bg left:40% 80% drop-shadow:0,0,0,#fff](img/IServ_Logo_White.png) Endlich ruhig schlafen bei der Frontend-Entwicklung. 😴 --- # Was ist Typescript? * Erweitert Javascript * Fügt Typeninformationen in die Sprache ein * Daten und Datenfluss wird besser beschrieben --- # Fehlt es Javascript an Types? ```js var data; // Was darf hier rein? ``` --- # Fehlt es Javascript an Types? ```js > var data = "some string"; > console.log(data); "some string" > data = 15; // Why not both? > console.log(data); 15 ``` --- # Jetzt mit Types! --- # Jetzt mit Types! ```ts var data: string = "some string"; console.log(data); data = 15; // ❌ Error console.log(data); ``` --- ## Transparenz von Schnittstellen Was tut diese Library? ```js // @iserv/graph.js export function drawGraph(data, options = {}) { [... Implementierungsdetails] } ``` --- ## Transparenz von Schnittstellen Was tut diese Library? ```js // @iserv/graph.js export function drawGraph(data, options = {}) { [... Implementierungsdetails] } ``` Besser: Wie benutze ich diese Library? --- ## Transparenz von Schnittstellen ```js // @iserv/graph.js export function drawGraph(data, options = {}) { [... Implementierungsdetails] } // your-module.js drawGraph([{ x: 2, y: 5, }, { x: 3, y: 7, }]); ``` --- ## Transparenz von Schnittstellen ```js // @iserv/graph.js export function drawGraph(data, options = {}) { [... Implementierungsdetails] } // your-module.js drawGraph([ 5, 7 ]); ``` --- ## Transparenz von Schnittstellen ```js // @iserv/graph.js export function drawGraph(data, options = {}) { [... Implementierungsdetails] } // your-module.js drawGraph([ 5, 7 ], { start: 0, stepSize: 1, }); ``` --- ## Dafür hat man doch JSDoc! ```js // @iserv/graph.js /** * @typedef {Object} GraphOptions * @property {HTMLCanvasElement} mount * @property {number} start * @property {number} stepSize * * @param {GraphOptions} options */ export function drawGraph(data, options = {}) { [...Implementierungsdetail] } // your-module.js drawGraph( data, { mount: document.getElementById('my-canvas'), start: 2, stepSize: 1, }, ) ``` --- ## Warum nicht JSDoc? ![bg left:50% 100%](img/self-documenting.png) * JSDoc kann lügen * Wird nicht automatisch überprüft --- ## Typescript aufsetzen ![bg left:50% 210%](img/busy-doggo.png) * In neuem Projekt * Integration mit Webpack --- ## Typescript in einem neuen Projekt Installieren ``` npm install -g typescript # global npm install --save-dev typescript # lokal ``` --- ## Typescript in einem neuen Projekt Neues Projekt ``` tsc --init ``` * (Lokal ist der Befehl dann `npx tsc [...]`) --- ## Konfiguration tsconfig.json ```json { "compilerOptions": { "target": "es2016", "module": "commonjs", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true } } ``` --- ## Kompilieren ``` tsc [] ``` --- ## Kompilieren ```ts // index.ts let variable: number = 5; variable = 10; ``` ```js // index.js "use strict"; let variable = 5; variable = 10; ``` --- ## Integration in Webpack-Projekten --- ## Integration in Webpack-Projekten ![bg 90%](img/webpack-typescript-bundling.png) --- ## Integration in Webpack-Projekten ``` npm install --save-dev typescript ts-loader ^^^^^^^^^ ``` --- ## Integration in Webpack-Projekten ```js // webpack.config.js module.exports = { module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: ['.tsx', '.ts', '.js'], }, // [...] }; ``` --- ## Integration in bestehende Projekte ![bg left:50% 100%](img/typescript-integration.png) --- ## Integration in bestehende Projekte ![bg left:50% 100%](img/typescript-integration.png) ![w:150](img/squarehole.png) --- ## Typescript in Javascript? * TS in JS integrieren geht ohne Probleme * ```ts // extension.ts export function getFileExtension(path: string): string { return path.split(".").slice(-1); } // path.js import {getFileExtension} from './extension.ts'; console.log(getFileExtension("some/image.png")); // > "png" ``` --- ## Javascript in Typescript? * JS in TS integrieren ist schwierig * ```ts // extension.js export function getFileExtension(path) { return path.split(".").slice(-1); } // path.ts import {getFileExtension} from './extension.ts'; console.log(getFileExtension("some/image.png")); ``` --- ## Javascript in Typescript ``` path.ts:1:32 - error TS7016: Could not find a declaration file for module './extension.js'. './extension.js' implicitly has an 'any' type. 1 import {getFileExtension} from './extension.js'; ~~~~~~~~~~~~~~~~ Found 1 error in path.ts:1 ``` --- ![bg 60%](img/typescript-migration-1.png) --- ![bg 60%](img/typescript-migration-2.png) --- ![bg 60%](img/typescript-migration-3.png) --- ## Javascript in Typescript Wie damit umgehen? * Typescript auf weniger strikt stellen * => Nutzung von Typescript verliert an Wert * Alle Abhängigkeiten direkt nach Typescript konvertieren * Nicht immer machbar * Schnittstellen mit Typen versehen --- ![bg 60%](img/typescript-migration-4.png) --- ## Schnittstellen typisieren ```ts // extension.d.ts export function getFileExtension(path: string): string; ``` * Keine Implementierung * Kein Type-Checking der Implementierung * Schnittstellen werden wenigstens typisiert `¯\_(ツ)_/¯` * Das geht auch mit JS-Libraries --- ## Wie beschreibe ich nun meine Daten? ```js const workerBee = { role: 'worker', name: 'Larry', age: 15, }; ``` --- ## Objekte ```ts const workerBee: Bee = { role: 'worker', name: 'Larry', age: 15, }; ``` ```ts type Bee = { role: string, name: string, age: number, }; ``` --- ## Semantische Typen ```ts const workerBee: Bee = { role: 'worker', name: 'Larry', age: 15, }; ``` ```ts type Bee = { role: BeeRole, name: string, age: Months, }; type BeeRole = 'worker'|'queen'|'guard'; type Months = number; ``` --- ![bg 60%](img/bee-type.png) --- ## Dictionaries ```js const userNames = { '652a4707-3043-4ca9-b417-24feac0f5953': 'Bernhard', '017daef7-e1f1-4160-81ff-e5b17e86fc6b': 'Frieda', }; ``` --- ## Dictionaries ```ts const userNames: UserNameDictionary = { '652a4707-3043-4ca9-b417-24feac0f5953': 'Bernhard', '017daef7-e1f1-4160-81ff-e5b17e86fc6b': 'Frieda', }; type UserId = string; type UserNameDictionary = Record; ``` --- ## Dictionaries (alternative) ```ts const userNames: UserNameDictionary = { '652a4707-3043-4ca9-b417-24feac0f5953': 'Bernhard', '017daef7-e1f1-4160-81ff-e5b17e86fc6b': 'Frieda', }; type UserId = string; type UserNameDictionary = { [UserId]: string; }; ``` --- ## Optionale Properties ```js const lkw = { anzahlRaeder: 18, hubraum: 25, }; const bobbycar = { anzahlRaeder: 4, }; ``` --- ## Optionale Properties ```ts const lkw: Car = { anzahlRaeder: 18, hubraum: 25, }; const bobbycar: Car = { anzahlRaeder: 4, }; type Kubikmeter = number; type Car = { anzahlRaeder: number, hubraum?: Kubikmeter, }; ``` --- ## Nur optionale Properties ```ts type Vehicle = Partial<{ anzahlRaeder: number, hubraum: Kubikmeter, anzahlKufen: number, }>; ``` ![bg right:60% 100%](img/bobbycar.png) ![bg right:60% 130%](img/hovercraft.png) ![bg right:60% 170%](img/hubschraubär.png) --- ## Nur optionale Properties ```ts type Vehicle = Partial<{ anzahlRaeder: number, hubraum: Kubikmeter, anzahlKufen: number, }>; const helikopter: Vehicle = { anzahlKufen: 2, }; ``` --- ## Nur optionale Properties ```ts type Vehicle = Partial<{ anzahlRaeder: number, hubraum: Kubikmeter, anzahlKufen: number, }>; const helikopter: Vehicle = { anzahlKufen: 2, }; const bobbyCar: Vehicle = { anzahlRaeder: 4, }; ``` --- ## Nur optionale Properties ```ts type Vehicle = Partial<{ anzahlRaeder: number, hubraum: Kubikmeter, anzahlKufen: number, }>; const helikopter: Vehicle = { anzahlKufen: 2, }; const bobbyCar: Vehicle = { anzahlRaeder: 4, }; const luftKissenFahrzeug: Vehicle = { hubraum: 10, }; ``` --- ## Readonly ```ts // @iserv/colors.ts export type Color = string; export type Colors = Record<'RED'|'GREEN'|'BLUE', Color> export const colors: Colors = { RED: '#ff0000', GREEN: '#00ff00', BLUE: '#0000ff', }; ``` --- ## Readonly ```ts // @iserv/colors.ts export type Color = string; export type Colors = Record<'RED'|'GREEN'|'BLUE', Color> export const colors: Colors = { RED: '#ff0000', GREEN: '#00ff00', BLUE: '#0000ff', }; ``` ```ts // consumer.ts import {colors} from '@iserv/colors.ts'; colors = {}; // verboten wegen const ``` --- ## Readonly ```ts // @iserv/colors.ts export type Color = string; export type Colors = Record<'RED'|'GREEN'|'BLUE', Color> export const colors: Colors = { RED: '#ff0000', GREEN: '#00ff00', BLUE: '#0000ff', }; ``` ```ts // consumer.ts import {colors} from '@iserv/colors.ts'; colors = {}; // verboten wegen const colors.BLUE = colors.RED; // erlaubt 😢 ``` --- ## Readonly ```ts // @iserv/colors.ts export type Color = string; export type Colors = Record<'RED'|'GREEN'|'BLUE', Color> export const colors: Readonly = { RED: '#ff0000', ^^^^^^^^ GREEN: '#00ff00', BLUE: '#0000ff', }; ``` ```ts // consumer.ts import {colors} from '@iserv/colors.ts'; colors = {}; // verboten wegen const colors.BLUE = colors.RED; // verbiet 😎 ``` --- ## Typen kombinieren ```ts type Renderable = { render: () => void, } type Updateable = { update: () => void, } const gameObject = { render: () => {}, update: () => {}, }; ``` --- ## Typen kombinieren - Union ```ts type Renderable = { render: () => void, } type Updateable = { update: () => void, } type GameObject = Renderable & Updateable; const gameObject: GameObject = { render: () => {}, update: () => {}, }; ``` --- ## Typen kombinieren ```ts type NetworkPacket = { data: Record, msg: string, }; type Exception = { trace: string[], msg: string, } ``` --- ## Typen kombinieren - Intersection ```ts type NetworkPacket = { data: Record, msg: string, }; type Exception = { trace: string[], msg: string, } type FetchResult = NetworkPacket | Exception; let fetch: () => FetchResult; const fetchResult: FetchResult = fetch(); ``` --- ## Typen kombinieren - Intersection ```ts type NetworkPacket = { data: Record, msg: string, }; type Exception = { trace: string[], msg: string, } type FetchResult = NetworkPacket | Exception; let fetch: () => FetchResult; const fetchResult: FetchResult = fetch(); console.log(fetchResult.msg); ``` --- ## Typen kombinieren - Intersection ```ts type NetworkPacket = { data: Record, msg: string, }; type Exception = { trace: string[], msg: string, } type FetchResult = NetworkPacket | Exception; let fetch: () => FetchResult; const fetchResult: FetchResult = fetch(); if ('trace' in fetchResult) { // fetchResult is Exception console.log(fetchResult.trace); } ``` --- ## Typen kombinieren - Intersection ```ts type NetworkPacket = { data: Record, msg: string, }; type Exception = { trace: string[], msg: string, } type FetchResult = NetworkPacket | Exception; let fetch: () => FetchResult; const fetchResult: FetchResult = fetch(); if ('data' in fetchResult) { // fetchResult is NetworkPacket console.log(fetchResult.data); } ``` --- ## Duck Typing ![bg right:60%](img/duck.png) * > *If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.* --- ## Type narrowing ```ts const ducks: unknown = await fetch('https://api.ducks.com/sorts.json'); ``` --- ## Type narrowing ```ts const ducks: unknown = await fetch('https://api.ducks.com/sorts.json'); type DuckSort = { name: string, id: number, species: string, habitat: string[], } ``` --- ## Type narrowing via Type-Guards ```ts const ducks: unknown = await fetch('https://api.ducks.com/sorts.json'); type DuckSort = { name: string, id: number, species: string, habitat: string[], } function isDuckSort(duckSort: unknown): duckSort is DuckSort { if (!('name' in duckSort) || typeof duckSort['name'] !== 'string') { return false; } // [...] } if (isDuckSort(ducks)) { // 'ducks' can be used now } ``` --- ![bg](img/manually.png) --- ## Automatische Type-Guard Generierung * `github.com/rhys-vdw/ts-auto-guard` * Generiert Type-Guards automatisch * ```ts /** @see {isDuckSort} ts-auto-guard:type-guard */ export type DuckSort = {/** ... */}; ``` * `npx ts-auto-guard` --- ## ts-auto-guard Beispiel ```ts export function isDuckSort(obj: unknown): obj is DuckSort { const typedObj = obj as DuckSort return ( (typedObj !== null && typeof typedObj === "object" || typeof typedObj === "function") && typeof typedObj["name"] === "string" && typeof typedObj["id"] === "number" && typeof typedObj["species"] === "string" && Array.isArray(typedObj["habitat"]) && typedObj["habitat"].every((e: any) => typeof e === "string" ) ) } ``` --- ## Bad Practices ```ts const config = readFromFile('config.yml') as Config; ``` * `as`-Operator ist der Holzhammer * Typescript glaubt dir alles * Führt potenzielle Bugs ein --- ## `as`-Operator ```ts const number = {name: 'blarg'} as number; ``` * ![trustmebro.png](img%2Ftrustmebro.png) * Fauler Ersatz für Type-Guarding * Panzerband für Typescripts Deduktions-Schwächen --- ## Deduktionsschwächen ```ts function inferBroken(str: string) { if (['Bernd', 'Margeret'].includes(str)) { const narrowed: 'Bernd'|'Margeret' = str; } } ``` --- ## Deduktionsschwächen ```ts function inferBroken(str: string) { if (['Bernd', 'Margeret'].includes(str)) { const narrowed: 'Bernd'|'Margeret' = str; } } ``` ``` error TS2322: Type 'string' is not assignable to type '"Bernd" | "Margeret"'. ``` --- ## Deduktionsschwächen - Do NOT ```ts function inferBroken(str: string) { if (['Bernd', 'Margeret'].includes(str)) { const narrowed = str as 'Bernd'|'Margeret'; } } ``` --- ## Deduktionsschwächen - Do NOT ```ts function inferBroken(str: string) { if (['Jürgen', 'Marilla'].includes(str)) { const narrowed = str as 'Bernd'|'Margeret'; // Wirft keine Fehler 😢 } } ``` * Mit guard-clauses wäre das nicht passiert --- ## Reicht jetzt auch ![bg right:60%](img/call-it-a-draw)