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:
- Offizielle Webseite von TypeScript
- TypeScript Roadmap - Roadmap zu neuen Features in vergangenen und künftigen Versionen von TypeScript
- TypeScript Handbook
- Doku zu tsconfig.json
- Wie man ein bestehendes Projekt schrittweise auf TypeScript umstellt:
- Blog-Artikel How we gradually migrated to TypeScript
- Blog-Artikel How to move your project to TypeScript - at your own pace
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:
- Ein Type Alias ist nur ein anderer Name für einen Typ. In Fehlermeldungen o.ä. steht dann der aufgelöste Typ (z.B.
string | null
) - Ein Type Alias kann nicht per
extends
oderimplements
erweitert werden. Daher sollte man Interfaces bevorzugen.
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:
T
ist ein "Indexable Type".keyof T
: Der Index-Typ (die möglichen Keys) vonT
.K extends keyof T
: DefiniertK
als den Index-Typ vonT
.T[K]
: Typ mit den möglichen Werten vonT
.
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
- Offizielles Tutorial React & Webpack
- Offizieller Quick-Start-Guide TypeScript React Starter
- awesome-typescript-loader
- Alternative: ts-loader
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>
)
}
}