Type the Script

by Łukasz Karpuć

Me

  • 10 years of professional experience
  • 6 years in Sabre
  • 2 years of persuading people to use types(cript)

Agenda

  • Taking the bull by the horns
  • TypeScript basics
  • Type systems 
  • The purpose

 

Type systems + TypeScript in general

Out of scope

  • Deep diving into type systems
  • Language tutorial
  • Advanced types in TypeScript

 

Anything not starting on "T"

Taking the bull by the horns

Text

"to do something difficult in a brave way"

JavaScript

class Turtle {
    give();
    save(data);   
}

class Tortoise {
    export();
    save(data);
}

let first = new Turtle();
let second = new Turtle();
let third = new Tortoise();

second.save(first.give());
first.save(second.give());
second.save(third.give());

// ERROR! 
// Undefined is not a function!

DUCK-TYPED JS

class Turtle {
    give();
    save(data);   
}

class Tortoise {
    export();
    save(data);
}

let first = new Turtle();
let second = new Turtle();
let third = new Tortoise();

if( typeof first.give === 'function' ) {
    second.save(first.give());
}

if( typeof second.give === 'function' )
    first.save(second.give());
}

if( typeof third.give === 'function' )
    second.save(third.give());
}

// But second one did not get data..

DUCK-TYPED JS

class Turtle {
    give();
    save(data);   
}

class Tortoise {
    export();
    save(data);
}

let first = new Turtle();
let second = new Turtle();
let third = new Tortoise();

if( typeof first.give === 'function' ) {
    second.save(first.give());
} else {
    second.save(first.export());
}

if( typeof second.give === 'function' )
    first.save(second.give());
} else {
    first.save(second.export());
}

if( typeof third.give === 'function' )
    second.save(third.give());
} else {
    second.save(third.export());
}

// But what if .export() returns 
// different data than .give()?

TypeScript

interface Data {
    // ...
}

class Turtle {
    give(): Data;
    save(data: Data);   
}

class Tortoise {
    export(): Data;
    save(data: Data);
}

let first = new Turtle();
let second = new Turtle();
let third = new Tortoise();

second.save(first.give());
first.save(second.give());
second.save(third.export());

// No syntax errors :)

TypeScript basics

Simple types

let motto: string = "abc";
let motto2 = "abc";

let counter: number = 1;
let counter2: number = 1.5;

let flag: boolean = true;

// has sense if strict null checks turned on
let flag2?: boolean = undefined;

let numbers: number[] = [1, 2, 3];

STILL Simple types

let tuple: [number, string, boolean] = [1, "text", true];

let simpleMap: {[key: string]: number} = {
    "one": 1,
    "two": 2
}

let customArray: {[idx: number]: number} = [1, 2, 3];

let something: any = 1;
something = "aaa";

let nothing: never = 1; // ERROR
let emptyArray: never[] = [];

// TS 3.1+
let strangeThing: unknown = "aaa";

something.charAt(0); // WORKS
strangeThing.charAt(0); // TS ERROR

Functions

function foo(a: number, b: string = "aaa"): void {
    console.log(a, b);
}

const bar: (a: number, b: string) => void = 
    (a: number, b: string = "aaa"): void => console.log(a, b);

OOP

enum E { A, B };

interface I {
    foo(): void;
}

abstract class Base implements I {
    private counter: number = 1;
    protected barAttr: () => void;

    constructor(private initial: E) {};

    public abstract foo(): void;
}

class Impl extends Base {
    public foo(): void {
        // ...
    }
}

Generics

interface X<T> {
    foo(): T;
}

class Y implements X<string> {
    foo(): string {
        return "123";
    }
}

/// functions also may use generics

function bar<T>(): T {
    // ...
};

Type Aliases

type A = string;
type B = { [key: string]: A };

let a: A = "abc";
let b: B = { "abc": "abc" };

type C = A | B;

let c: C;
c = a;
c = b;
c = Math.random(); // Type 'number' is not assignable to type 'C'

type D = A & B;

let d: D;
d = a; // Type 'string' is not assignable to type 'B'
d = b; // Type 'B' is not assignable to type 'string'

Type SUBSETS

type StringSubset = 'a' | 'b' | 'c';

let foo: StringSubset;
foo = 'a'; // OK
foo = 'd'; // ERROR

type NumberSubset = 1 | 2 | 3;

let bar: NumberSubset;
bar = 1; // OK
bar = 0; // ERROR

bar = 1;
bar--;
bar === 0; // TRUE

// types works before run-time only

Before Run-time?

What does it mean?

Type systems

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural
  • manifest vs type inference            

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural
  • manifest vs type inference            

 

Static typing works before run-time.

Dynamic typing works in run-time.

 

 

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural
  • manifest vs type inference            

 

// STATIC

let a: number; // type set here

a = 1;
// DYNAMIC

let a;

a = 1; // type set here

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural
  • manifest vs type inference            

 

Strong typing makes type of variable immutable.

Weak typing allows type of variable to be mutable.

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural
  • manifest vs type inference            

 

// STRONG

let a: number;

a = 1;
a = "abc"; // ERROR
// WEAK

let a;

a = 1; // type becomes number
a = "abc"; // OK, type becomes string

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural
  • manifest vs type inference            

 

Nominal typing uses type name before run-time.

Structural typing uses type structure before run-time.

 

Duck-typing is a technique that checks object structure in run-time.

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural
  • manifest vs type inference            

 

// NOMINAL

class A {};
class B {};

let a: A = new B(); // ERROR
// STRUCTURAL

class A {};
class B {};

let a: A = new B(); // OK...

// ...structures match

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural                     
  • manifest vs type inference

 

Manifest typing reads explicit type names from source code.

Type inference deduces types from code context.

 

Both works before run-time.

 

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural                     
  • manifest vs type inference

 

// MANIFEST

let a: string = "abc";
a = "def";
a = 1; // ERROR

// TYPE INFERENCE

let a = "abc"; // type == string
a = "def";
a = 1; // ERROR

// faster coding

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural                     
  • manifest vs type inference

 

// MANIFEST

let a: string;
a = "abc";
a = "def";
a = 1; // ERROR

// TYPE INFERENCE

let a;  // type == any
a = "abc";
a = "def";
a = 1; // still OK, be careful

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural                     
  • manifest vs type inference

 

// MANIFEST

function(): number { return 1 };

// re-implemented into 

function(): number { return "A" };

// ERROR - interfaces stay safe
// TYPE INFERENCE

function() { return 1 }; // ()=>number

// re-implemented into 

function() { return "A" }; // ()=>string

// NO ERROR - interfaces broken

TYPE SYSTEMS

  • static vs dynamic
  • strong vs weak
  • nominal vs structural
  • manifest vs type inference            

TypeScript's

TYPE SYSTEM...

  • static vs dynamic
  • strong vs weak
  • nominal vs structural             
  • manifest vs type inference            

 

 

 

THE PURPOSE

The PURPOSE

  • Manifesting intentions
  • Freeing mind
  • Decomposing systems
  • Helping IDE to help you
  • Making tests easier
  • Simplifying design patterns

Manifesting intentions

Manifesting intentions


function setId(id);

Manifesting intentions


function setId(id);

// what is ID?
// should I check docs?
// should I call author?
// should I try number?
// should I guess?

Manifesting intentions


function setId(id: string);

FREEING MIND

FREEING MIND

enum DevThings {DESIGN, TYPES, TYPE_CHECKING, CODE};

const HEAD: DevThings[] = [];
const COMPILER: DevThings[] = [];
const RUNTIME: DevThings[] = [];
const CODEBASE: DevThings[] = [];

FREEING MIND

enum DevThings {DESIGN, TYPES, TYPE_CHECKING, CODE};

const HEAD: DevThings[] = [];
const COMPILER: DevThings[] = [];
const RUNTIME: DevThings[] = [];
const CODEBASE: DevThings[] = [];

// no code typing?

HEAD.push(DevThings.DESIGN);
HEAD.push(DevThings.TYPES);
HEAD.push(DevThings.TYPE_CHECKING);

RUNTIME.push(DevThings.TYPE_CHECKING);

CODEBASE.push(DevThings.CODE);

FREEING MIND

enum DevThings {DESIGN, TYPES, TYPE_CHECKING, CODE};

const HEAD: DevThings[] = [];
const COMPILER: DevThings[] = [];
const RUNTIME: DevThings[] = [];
const CODEBASE: DevThings[] = [];

// save time and CPU ;)

HEAD.push(DevThings.DESIGN);

COMPILER.push(DevThings.TYPE_CHECKING);

CODEBASE.push(DevThings.CODE);
CODEBASE.push(DevThings.TYPES);

DECOMPOSING SYSTEMS

DECOMPOSING SYSTEMS

system A {
    function foo() { /* .. */ };

    let b = new B();
    b.foo(); // I wonder how they called .foo() method
}

DECOMPOSING SYSTEMS

system A {
    function foo() { /* .. */ };

    let b = new B();
    b.foo(); // I wonder how they called .foo() method
}

system B {
    function bar() { /* .. */ };

    let a = new A();
    a.bar(); // I wonder how they called .bar() method
}

DECOMPOSING SYSTEMS

system A {
    interface A { /* .. */ };
    function foo() { /* .. */ };

    let b: B = new B();
    b.bar();
}

system B {
    interface B { /* .. */ };
    function bar() { /* .. */ };

    let a: A = new A();
    a.foo();
}

HELPING IDE TO HELP YOU

HELPING IDE TO HELP YOU

IDE and compiler know about types so they can:

  • make better refactorings
  • auto-complete code
  • list proper method names, not guessed ones
  • show errors before code execution
  • create diagrams
  • help you to improve design
  • point out common and uncommon mistakes

MAKING TESTS EASIER

MAKING TESTS EASIER

it('checks input types', () => {
    expect(() => {
        stringsOnly(1);
    }).toThrow();

    expect(() => {
        stringsOnly(true);
    }).toThrow();


    expect(() => {
        stringsOnly([1, 2, 3]);
    }).toThrow();

    // ...
});

MAKING TESTS EASIER

SIMPLIFYING DESIGN PATTERNS

class BaseStrategy {
    foo();
}

class AlternativeStrtegy {
    foo();
}

function useStrategy(strategy) {
    strategy.foo();
}

SIMPLIFYING DESIGN PATTERNS

class BaseStrategy {
    foo();
    bar();
}

class AlternativeStrtegy {
    foo();
}

function useStrategy(strategy) {
    strategy.foo();
    strategy.bar();
}

// OOPS.. forgotting about AlternativeStrategy..

SIMPLIFYING DESIGN PATTERNS

interface Strategy {
    foo();
    bar();
}

class BaseStrategy implements Startegy {
    foo();
    bar();
}

class AlternativeStrtegy implements Strategy {
    foo();
}

function useStrategy(strategy: Strategy) {
    strategy.foo();
    strategy.bar();
}

// AlternativeStrategy not forgotten! Compile-time error. 
// Class be modified. World is safe now!

Any questions?

THE END

Any questions?

Maybe... what tools/languages would you recommend?

Any questions?

Maybe... what tools/languages would you recommend?

                    

TypeScript

https://typescriptlang.org

 

  • made by Microsoft
  • works with JS
  • separate but sensitive
  • easy to use for JS programmers

Flow

https://flow.org

 

  • made by Facebook
  • works with JS
  • real add-on
  • easy to implement in legacy project

Dart

https://www.dartlang.org

 

  • made by Google
  • separate language
  • closed system
  • keeps the code in leash better

Kotlin

https://kotlinlang.org

 

  • made by JetBrains
  • multi-platform language
  • enables future-proof designs
  • keeps the code in leash better

THE END

Any questions?

Maybe... where to find guides for TypeScript?

Any questions?

Maybe... where to find guides for TypeScript?

 

THE END

Any questions?

Maybe... how to avoid `any` type?

Any questions?

Maybe... how to avoid `any` type?

 

// BAD

function stringify(sth: any) { /* ... */ };

Any questions?

Maybe... how to avoid `any` type?

 

// BETTER

function stringify(sth: string) { /* ... */ };
function stringify(sth: number) { /* ... */ };
function stringify(sth: boolean) { /* ... */ };
function stringify(sth: null) { /* ... */ };
function stringify(sth: Array<any>) { /* ... */ };
function stringify(sth: {[key: string]: any}) { /* ... */ };

Any questions?

Maybe... how to avoid `any` type?

 

// BEST

type JsonType = JsonPrimitive | JsonArray | JsonObject;
type JsonPrimitive = string | number | boolean | null;

interface JsonObject {
    [key: string]: JsonType
}

interface JsonArray {
    [key: number]: JsonType
}

function stringify(sth: JsonType): string { /* ... */ };

THE END

Any questions?

Maybe... how to use `any` type?

 

Any questions?

Maybe... how to use `any` type?

// BAD
applyMapper(fn: Function): any;

// A LITTLE BETTER
applyMapper<TOut>(fn: Function): TOut;

// OK
applyMapper<TIn, TOut>(fn: (input: TIn) => TOut): TOut;

// OK WITH BACKWARD COMPATIBILTY
applyMapper<TIn = any, TOut = any>(fn: (input: TIn) => TOut): TOut;

THE END

Any questions?

Maybe... how to deal with `object` type?

Any questions?

Maybe... how to deal with `object` type?

// PROBABLY NOT WHAT YOU WANT TO USE

// this one is just everything...

let o1: Object = anything;
let o2: {} = sameAsObject;

// this one is limited to non-primitives 
// but also is too wide

let o3: object = nonPrimitive;

Any questions?

Maybe... how to deal with `object` type?

// PROBABLY YOU WANT THIS ONE FOR JSON-LIKE STRUCTURES

type JsonType = JsonPrimitive | JsonArray | JsonObject;
type JsonPrimitive = string | number | boolean | null;

interface JsonObject {
    [key: string]: JsonType
}

interface JsonArray {
    [idx: number]: JsonType
}

Any questions?

Maybe... how to deal with `object` type?

// PROBABLY YOU WANT THIS ONE FOR SIMPLE KEY-VALUE MAPS

interface KeyValueMap<V = any> {
    [key: string]: V;
}

THE END

Any questions?

Maybe... how to use interfaces in run-time?

Any questions?

Maybe... how to use interfaces in run-time?

// interface is compile-time thing so this will not work

interface X { /* ... */ }

class C implements X { /* ... */ }

registerAsImplementation(X, C);

// TS compiler error:
// 'X' only refers to a type, but is being used as a value here.

Any questions?

Maybe... how to use interfaces in run-time?

// however you may use classes as interfaces...

abstract class X { /* ... */ }

class C implements X { /* ... */ }

registerAsImplementation(X, C);

// WORKS!

// Use fully abstract classes only
// It is not 100% elegant but works well

THE END

Any questions?

Maybe... how to properly use type assertion?

 

Any questions?

Maybe... how to properly use type assertion?

 

declare function foo(): SomeType | OtherType;

// BAD

(foo() as SomeType).someMethod();

Any questions?

Maybe... how to properly use type assertion?

 

declare function foo(): SomeType | OtherType;

// OK

function isSomeType(obj: SomeType | OtherType): obj is SomeType {
    return typeof (obj as SomeType).someMethod === 'function';
}

let fooValue = foo();

if( isSomeType(fooValue) ) {
    fooValue.someMethod();
}

THE END

Any questions?

Really last call.. any questions?

THE END

Type the Script

By Lukasz K

Type the Script

  • 347