Files
2023-07-03 15:16:41 +02:00

15 KiB

theme, _class, paginate, backgroundColor, color
theme _class paginate backgroundColor color
gaia lead true

Typescript

bg left:40% 80% drop-shadow:0,0,0,#fff

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?

var data; // Was darf hier rein?

Fehlt es Javascript an Types?

> var data = "some string";
> console.log(data);
"some string"
> data = 15; // Why not both?
> console.log(data);
15


Jetzt mit Types!


Jetzt mit Types!

var data: string = "some string";
console.log(data);
data = 15; // ❌ Error
console.log(data);

Transparenz von Schnittstellen

Was tut diese Library?

// @iserv/graph.js
export function drawGraph(data, options = {}) {
    [... Implementierungsdetails]
}

Transparenz von Schnittstellen

Was tut diese Library?

// @iserv/graph.js
export function drawGraph(data, options = {}) {
    [... Implementierungsdetails]
}

Besser: Wie benutze ich diese Library?


Transparenz von Schnittstellen

// @iserv/graph.js
export function drawGraph(data, options = {}) {
    [... Implementierungsdetails]
}

// your-module.js
drawGraph([{
    x: 2,
    y: 5,
}, {
    x: 3,
    y: 7,
}]);

Transparenz von Schnittstellen

// @iserv/graph.js
export function drawGraph(data, options = {}) {
    [... Implementierungsdetails]
}

// your-module.js
drawGraph([
    5,
    7
]);

Transparenz von Schnittstellen

// @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!

// @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%

  • JSDoc kann lügen
  • Wird nicht automatisch überprüft

Typescript aufsetzen

bg left:50% 210%

  • 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

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Kompilieren

tsc [<file>]

Kompilieren

// index.ts
let variable: number = 5;

variable = 10;

<one tsc later>

// index.js
"use strict";
let variable = 5;
variable = 10;

Integration in Webpack-Projekten


Integration in Webpack-Projekten

bg 90%


Integration in Webpack-Projekten

npm install --save-dev typescript ts-loader
                                  ^^^^^^^^^

Integration in Webpack-Projekten

// 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%


Integration in bestehende Projekte

bg left:50% 100%

w:150


Typescript in Javascript?

  • TS in JS integrieren geht ohne Probleme
  • // 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
  • // 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%


bg 60%


bg 60%


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%


Schnittstellen typisieren

// 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?

const workerBee = {
    role: 'worker',
    name: 'Larry',
    age: 15,
};

Objekte

const workerBee: Bee = {
    role: 'worker',
    name: 'Larry',
    age: 15,
};
type Bee = {
    role: string,
    name: string,
    age: number,
};

Semantische Typen

const workerBee: Bee = {
    role: 'worker',
    name: 'Larry',
    age: 15,
};
type Bee = {
    role: BeeRole,
    name: string,
    age: Months,
};
type BeeRole = 'worker'|'queen'|'guard';
type Months = number;

bg 60%


Dictionaries

const userNames = {
    '652a4707-3043-4ca9-b417-24feac0f5953': 'Bernhard',
    '017daef7-e1f1-4160-81ff-e5b17e86fc6b': 'Frieda',
};

Dictionaries

const userNames: UserNameDictionary = {
    '652a4707-3043-4ca9-b417-24feac0f5953': 'Bernhard',
    '017daef7-e1f1-4160-81ff-e5b17e86fc6b': 'Frieda',
};
type UserId = string;
type UserNameDictionary = Record<UserId, string>;

Dictionaries (alternative)

const userNames: UserNameDictionary = {
    '652a4707-3043-4ca9-b417-24feac0f5953': 'Bernhard',
    '017daef7-e1f1-4160-81ff-e5b17e86fc6b': 'Frieda',
};
type UserId = string;
type UserNameDictionary = {
    [UserId]: string;
};

Optionale Properties

const lkw = {
    anzahlRaeder: 18,
    hubraum: 25,
};
const bobbycar = {
    anzahlRaeder: 4,
};

Optionale Properties

const lkw: Car = {
    anzahlRaeder: 18,
    hubraum: 25,
};
const bobbycar: Car = {
    anzahlRaeder: 4,
};
type Kubikmeter = number;
type Car = {
    anzahlRaeder: number,
    hubraum?: Kubikmeter,
};

Nur optionale Properties

type Vehicle = Partial<{
    anzahlRaeder: number,
    hubraum: Kubikmeter,
    anzahlKufen: number,
}>;

bg right:60% 100% bg right:60% 130% bg right:60% 170%


Nur optionale Properties

type Vehicle = Partial<{
    anzahlRaeder: number,
    hubraum: Kubikmeter,
    anzahlKufen: number,
}>;
const helikopter: Vehicle = {
    anzahlKufen: 2,
};

Nur optionale Properties

type Vehicle = Partial<{
    anzahlRaeder: number,
    hubraum: Kubikmeter,
    anzahlKufen: number,
}>;
const helikopter: Vehicle = {
    anzahlKufen: 2,
};
const bobbyCar: Vehicle = {
    anzahlRaeder: 4,
};

Nur optionale Properties

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

// @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

// @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',
};
// consumer.ts
import {colors} from '@iserv/colors.ts';
colors = {}; // verboten wegen const

Readonly

// @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',
};
// consumer.ts
import {colors} from '@iserv/colors.ts';
colors = {}; // verboten wegen const
colors.BLUE = colors.RED; // erlaubt 😢

Readonly

// @iserv/colors.ts
export type Color = string;
export type Colors = Record<'RED'|'GREEN'|'BLUE', Color>
export const colors: Readonly<Colors> = {
    RED: '#ff0000',  ^^^^^^^^
    GREEN: '#00ff00',
    BLUE: '#0000ff',
};
// consumer.ts
import {colors} from '@iserv/colors.ts';
colors = {}; // verboten wegen const
colors.BLUE = colors.RED; // verbiet 😎

Typen kombinieren

type Renderable = {
    render: () => void,
}
type Updateable = {
    update: () => void,
}

const gameObject = {
    render: () => {},
    update: () => {},
};

Typen kombinieren - Union

type Renderable = {
    render: () => void,
}
type Updateable = {
    update: () => void,
}
type GameObject = Renderable & Updateable;
const gameObject: GameObject = {
    render: () => {},
    update: () => {},
};

Typen kombinieren

type NetworkPacket = {
    data: Record<string, string>,
    msg: string,
};
type Exception = {
    trace: string[],
    msg: string,
}

Typen kombinieren - Intersection

type NetworkPacket = {
    data: Record<string, string>,
    msg: string,
};
type Exception = {
    trace: string[],
    msg: string,
}
type FetchResult = NetworkPacket | Exception;
let fetch: () => FetchResult;
const fetchResult: FetchResult = fetch();

Typen kombinieren - Intersection

type NetworkPacket = {
    data: Record<string, string>,
    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

type NetworkPacket = {
    data: Record<string, string>,
    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

type NetworkPacket = {
    data: Record<string, string>,
    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%

  • If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.


Type narrowing

const ducks: unknown = await fetch('https://api.ducks.com/sorts.json');

Type narrowing

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

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


Automatische Type-Guard Generierung

  • github.com/rhys-vdw/ts-auto-guard
  • Generiert Type-Guards automatisch
  • /** @see {isDuckSort} ts-auto-guard:type-guard */
    export type DuckSort = {/** ... */};
    
  • npx ts-auto-guard

ts-auto-guard Beispiel

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

const config = readFromFile('config.yml') as Config;
  • as-Operator ist der Holzhammer
  • Typescript glaubt dir alles
  • Führt potenzielle Bugs ein

as-Operator

const number = {name: 'blarg'} as number;
  • trustmebro.png
  • Fauler Ersatz für Type-Guarding
  • Panzerband für Typescripts Deduktions-Schwächen

Deduktionsschwächen

function inferBroken(str: string) {
    if (['Bernd', 'Margeret'].includes(str)) {
        const narrowed: 'Bernd'|'Margeret' = str;
    }
}

Deduktionsschwächen

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

function inferBroken(str: string) {
    if (['Bernd', 'Margeret'].includes(str)) {
        const narrowed = str as 'Bernd'|'Margeret';
    }
}

Deduktionsschwächen - Do NOT

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%