TypeScript

Eine Snippet-Sammlung für TypeScript.

TypeScript ist eine von Microsoft entwickelte Erweiterung von ES6, die im Wesentlichen statische Typisierung hinzufügt.

Microsoft hat außerdem einen Open-Source TypeScript-Compiler entwickelt, der TypeScript in ES5 (oder ES3 oder ES6) übersetzen kann. Der Compiler lässt sich einfach per API ansprechen. So kommt es, dass viele bekannte Editoren und IDEs weitreichenden TypeScript-Support bieten.

Links:

TODO Excess Property Checks

Snippets

Klassen:

interface MyInterface extends A, B {
    id: number;
    setName(name: string): void;
}

class MyClass implements MyInterface, C {

    static myStaticProp: number = 1    // Statische Property

    id: number                         // Normale Property
    private myPrivate: number = 5      // Nur in dieser Klasse sichtbar
    protected myProtected: number = 5  // Sichtbar für diese und Kindklassen
    readonly myReadOnly: number = 5    // Muss bei Deklaration oder im Konstruktor initialisiert werden

    // "Parameter properties":
    // - Fügt man `public`, `protected` oder `private` hinzu, wird eine
    //   Property angelegt und mit dem Parameter-Wert initialisiert.
    // - Kann mit `readonly` kombiniert werden.
    //   (`public` ist Default und kann auch weggelassen werden)
    constructor(public name: string, readonly age: number) {
    }

    setName(name: string) {
    }

    addUser(person: { name: string, age?: number }): void {
    }
}

Accessors:

class MyClass {
    private _name: string

    get name(): string {
        ...
        return this._name
    }

    set name(newName: string) {
        ...
        this._name = newName
    }
}

Abstrakte Klassen:

abstract class MyAbstractClass {
    abstract myAbstractMethod(): void;
}

Funktionen mit zusätzlichen Attributen:

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

Typ-System

Basis-Typen

let isDone: boolean = false
let count: number = 6
let color: string = 'blue'
let obj: Object = {}          // Auch mögliche Werte: 5, '', ...

any (jeder Typ):

let untyped: any = 4
untyped.toFixed()     // OK, wird von Compiler nicht geprüft
let obj: Object = 4
obj.toFixed()         // Fehler, `Object` hat kein `toFixed`

void (ohne Typ):

function foo(): void {}

null und undefined:

let u: undefined = undefined  // Nur `undefined` ist gültig
let n: null = null            // Nur `null` ist gültig

never:

function throwError(): never {
    throw Error('my error')
}
function fail() {   // Compiler erkennt Return-Typ `never`
    while (true) {
    }
}

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
switch (foo) {
    case ...;
    default: return assertNever(foo); // Fehler, wenn nicht alle Fälle abgedeckt sind
}

this:

Der Typ this repräsentiert den Instanz-Typ der umgebenden Klasse oder Schnittstelle. Instanz-Typ heißt, dass bei Vererbung der Typ der Kindklasse gilt, auch wenn die betreffende Methode nicht überschrieben wurde.

class MyClass {
    myChainingMethod(): this {
        ...
        return this
    }
}

Typen kombinieren (Union-Type)

let a: number | string = 'Bla'
let b: (number | null)[] = [1, 2, null]

Datenstrukturen

// Objects (auch: Interfaces)
let person: { name: string, age?: number } = { name: 'Anna Bolika' }  // `age` ist optional

// Array
let list: number[] = [1, 2, 3]

// ReadonlyArray
let ro: ReadonlyArray<number> = a;
ro[0] = 12                        // Fehler!
ro.push(5)                        // Fehler!
ro.length = 100                   // Fehler!
let a: number[] = ro;             // Fehler!
let a: number[] = ro as number[]  // OK (erzeugt keinen Klon - readonly wird umgangen!)

// Tuple
let x: [string, number] = ['hello', 10]
console.log(x[0])  // 'hello'
x[3] = 'world'     // Ist möglich. Einträge außerhalb des definierten Bereichs haben Union-Type (hier: string | number)

// Enum
enum Color {Red, Green, Blue}          // Red === 0, Green === 1, Blue === 2
enum Color {Red = 1, Green = 4, Blue}  // Red === 1, Green === 4, Blue === 5
let c: Color = Color.Green
let colorName: string = Color[c]       // 'Green'

Type assertions

Type assertions sind ähnlich zu Casts anderer Sprachen, haben jedoch keine Laufzeit-Auswirkung wie Checks oder Datenumwandlung.

// "angle-bracket"-Syntax - funktioniert nicht in `.tsx`-Dateien (JSX)
let someValue: any = "this is a string"
let strLength: number = (<string>someValue).length

// "as"-Syntax
let someValue: any = "this is a string"
let strLength: number = (someValue as string).length

Interfaces

interface Person {
    readonly id: number;
    name: string;
    age?: number;
}

function addUser(person: Person) {
    ...
    person.id = 5   // Fehler: `id` ist readonly
}

addUser({ id: 42, name: 'Anna Bolika' })  // OK: `age` ist optional

Interfaces müssen nicht wie in anderen Sprachen explizit per implements benannt werden. Duck-typing (aka structural typing) geht auch:

interface Named {
    name: string
}
class Person {
    name: string
}

let p: Named
p = new Person()   // OK, durch structural typing

Klassen-Typen (per typeof)

class MySubClass extends MyClass { ... }
let myClass: typeof MyClass = MySubClass
let myInstance: MyClass = new myClass()

Function types

interface SearchFunc {
    (source: string, subString: string): boolean;
}

// OK: Parameter-Namen müssen nicht passen
let mySearch: SearchFunc = function(src: string, sub: string): boolean {
    ...
}

// Mit "contextual typing" (verwendet die Typen aus dem Interface)
let mySearch: SearchFunc = function(src, sub) {
    ...
}
let myFunction: (x: number, y: number) => number
myFunction = function(x: number, y: number): number { ... }

// Auch möglich - andere Parameternamen und Typen per "contextual typing":
myFunction = function(theX, theY) {...}

// Auch möglich: Function type in "call signature"-Syntax
let myFunction: {(x: number, y: number): number}

Parameter:

// - Parameter `age` ist optional.
// - Parameter `type` ist optional mit Default-Wert.
//   Typ ist `string` per "contextual typing".
addUser(age?: number, type = 'user'): void {
}

Indexable Types

interface StringArray {
    [index: number]: string   // Index kann `number` oder `string` sein
}
let myArray: StringArray = ["Bob", "Fred"]
let myStr: string = myArray[0]

Mit optionalen Keys:

const myMap: { [key in string]?: MyType } = {}
const item = myMap['bla']    // MyType | undefined 

Readonly geht auch:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"]
myArray[2] = "Mallory"  // Fehler!

Intersection types

function extend<T, U>(first: T, second: U): T & U { ... }

Der Typ T & U enthält alle Properties von T und U (Mischung).

Type guards

// User-defined type guard
function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

if (isFish(pet)) {
    pet.swim()  // OK, type is considered to be `Fish`
} else {
    pet.fly()   // OK, type is considered to be `Bird`
}

Man kann den Typ auch mit bestimmten JavaScript-Ausdrücken einschränken:

if (typeof padding === "number") { ... }  // `typeof` type guard
if (foo instanceof MyClass) { ... }  // `instanceof` type guard
if (foo != null) { ... }
let a = foo || 'default'

In TypeScript gibt es außerdem die identifier!-Syntax, welche null und undefined von einem Typ entfernt:

let a: string | null = ...
return a.chartAt(0)   // Fehler (mit Strict null checks): `a` könnte null sein
return a!.chartAt(0)  // OK (`identifier!`-Syntax)

Type-Definitionen

Type Aliases:

type Name = string;
type NullableName = Name | null;   // string | null

Unterschiede von Type Aliases zu Interfaces:

String Literal Types:

type Easing = "ease-in" | "ease-out" | "ease-in-out"

// Overloads mit String Literal Types
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
function createElement(tagName: string): Element {
    ...
}

Discriminated Unions

Kombination aus Literal Types, Union Types, Type guards und Type aliases.

// Mehrere Typen mit einer gemeinsame String-Property (dem "discriminant")
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

// Einen Type Alias, der diese Typen vereint (die "union")
type Shape = Square | Rectangle | Circle;

// Type guards für die gemeinsame String-Property
// Wichtig: Return-Wert definieren, dann kommt ein Fehler, falls `Shape`
//          um einen weiteren Typ erweitert wird
function area(s: Shape): number {
    switch (s.kind) {
        case "square":    return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle":    return Math.PI * s.radius ** 2;
    }
    // Unreachable code (außer `Shape` wird erweitert)
}

Typisierte Keys

type MyKey = 'foo' | 'bar'

// Alle Keys müssen definiert sein:
const myMap: { [K in MyKey]: number } = {
    foo: 1,
    bar: 2
}

// Keys dürfen fehlen (wegen `?`):
const myMap2: { [K in MyKey]?: number } = {
    foo: 1
}

Der "keyof typeof"-Trick

Typ erzeugen, der die Keys einer Objekt-Konstanten beschreibt:

const myMap = { a: 1, c: 6, f: 3 }

type MyMapKeys = keyof typeof myMap

// Äquivalent:
type MyMapKeys = 'a' | 'c' | 'f'

Strict null checks

Aus historischen Gründen darf per Default jeder Wert null oder undefined sein:

let a: number = null   // OK, wenn ohne "strict null checks"

Es ist jedoch besser, wenn man "Strict null checks" aktiviert:

tsc --strictNullChecks src/entry.ts
let a: number = null         // Fehler mit "strict null checks"
let b: number | null = null  // OK

Man kann "Strict null checks" auch in tsconfig.json aktivieren:

{
    "compilerOptions": {
        ...
        "strictNullChecks": true
    },
    ...
}

Generics

Basics

function identity<T>(arg: T): T {
    return arg;
}
let output = identity<string>("myString")

// Typisierung
let foo: <T>(arg: T) => T = identity
let foo: {<T>(arg: T): T} = identity   // Äquivalent als "call signature"

// Contraints
class MyClass<T extends MyClass> { ... }
function create<T>(c: {new(): T; }): T {
    return new c()
}

Typisierung und Constraints bei "Indexable Types"

Details siehe Doku über Index types

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
    return names.map(n => o[n])
}

Hinweise:

Mapped types

Details siehe Doku zu Mapped types

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

// Äquvalent:
type Flags = { option1: boolean; option2: boolean; }
type Readonly<T> = {
    readonly [P in keyof T]: T[P];  // Macht jede Property von T readonly
}
type Partial<T> = {
    [P in keyof T]?: T[P];   // Macht jede Property von T optional (per `?`)
}

// Verwendung
type PartialPerson = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

Namespaces

Namespaces sind benannte JavaScript-Objekte im globalen Namespace.

Namespaces erzeugen Global Namespace Pollution und führen dazu, dass Abhängigkeiten nicht explizit deklariert werden. Daher sollte man für größere Projekte Module verwenden. Namespace eignen sich jedoch für kleine Projekte, da man damit in einer ES5-Umgebung keinen Module Loader benötigt.

namespace MyNamespace {
    export const name = 'Otto'
}

console.log(MyNamespace.name)

import myName = MyNamespace.name
console.log(myName)

Namespaces über mehrere Dateien:

// foo.ts

namespace MyNamespace {
    export const foo = 'foo'
}
// bar.ts

/// <reference path="foo.ts" />
namespace MyNamespace {
    export function bar() { return foo }
}

Ambient Modules (API-Definitionen)

Für bestehenden (JavaScript-)Code kann man API-Definitionen (.d.ts-Dateien, auch Ambient Modules genannt) anlegen. Diese enthalten - ähnlich zu einem Header-File nur die Definition eines Moduls ohne Implementierung.

Die Community hat für viele bestehende Bibliotheken bereits solche Ambient Modules erstellt. Unter npm heißen diese üblicherweise @types/my-lib.

Details siehe Doku zu Ambient Modules.

// node.d.ts (simplified)

declare module "url" {
    export interface Url {
        protocol?: string;
        hostname?: string;
        pathname?: string;
    }

    export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}

Verwendung:

/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");

Namespaces definieren:

// D3.d.ts (simplified)

declare namespace D3 {
    export interface Event {
        x: number;
        y: number;
    }

    export interface Base {
        event: Event;
    }
}

declare var d3: D3.Base;

TypeScript direkt per CLI compilieren

TypeScript-Compiler global installieren:

npm install -g typescript

Quellcode übersetzen (mit -w für Watch):

tsc -w src/entry.ts

Alternativ kann man den TypeScript-Compiler auch lokal installieren und ausführen:

npm install typescript
node_modules/typescript/bin/tsc -w src/entry.ts

Quellcode mit Abhängigkeiten in eine (konkatinierte) Datei übersetzen:

tsc --outFile my-app.js src/entry.ts

TypeScript mit Webpack und React

TODO

Projekt anlegen:

npm init
npm install --save-dev webpack typescript awesome-typescript-loader source-map-loader
npm install --save react react-dom @types/react @types/react-dom

Hinweis: Die Pakete mit dem Präfix @types/ enthalten TypeScript-Definitionen für die entsprechenden Pakete.

TypeScript-Konfiguration tsconfig.json anlegen (Details siehe Doku zu tsconfig.json):

// tsconfig.json

{
    "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "noImplicitAny": true,
        "strictNullChecks": true,
        "module": "commonjs",
        "target": "es5",
        "jsx": "react"
    },
    "include": [
        "./src/**/*"
    ]
}

Webpack-Konfiguration webpack.config.js anlegen:

// webpack.config.js

module.exports = {
    entry: "./src/index.tsx",
    output: {
        filename: "bundle.js",
        path: __dirname + "/dist"
    },

    // Enable sourcemaps for debugging webpack's output.
    devtool: "source-map",

    resolve: {
        // Add '.ts' and '.tsx' as resolvable extensions.
        extensions: [".ts", ".tsx", ".js", ".json"]
    },

    module: {
        rules: [
            // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
            { test: /\.tsx?$/, loader: "awesome-typescript-loader" },

            // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
            { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
        ]
    },

    // When importing a module whose path matches one of the following, just
    // assume a corresponding global variable exists and use that instead.
    // This is important because it allows us to avoid bundling all of our
    // dependencies, which allows browsers to cache those libraries between builds.
    externals: {
        "react": "React",
        "react-dom": "ReactDOM"
    },
}

Zitat aus Quelle:

You might be wondering about that externals field. We want to avoid bundling all of React into the same file, since this increases compilation time and browsers will typically be able to cache a library if it doesn’t change.

Template für React-Komponente:

// MyView.tsx

import React, { Component } from 'react'
import classNames from 'classnames'

import './MyView.less'

export interface Props {
    className: string
    style: any  // TODO
}

interface State {
}

export default class MyView extends Component<Props, State> {
    render() {
        const props = this.props
        return (
            <div className={classNames(props.className, 'MyView')} style={props.style}>
            </div>
        )
    }
}