How to model your 🤪 data with

Michał Michalczuk

TypeScript

JavaScript that scales

TypeScript

ES6

ES5

  • Superset of JS
     
  • Compiles to JS
     
  • JavaScript is a legal TypeScript
     
  • Optionally statically typed

Type systems

Lang Types check Run time safety



 
Dynamic Weak



 
Static Weak



 
Static Strong

No extra memory footprint

TypeScript type system

Structural type system*

As strict as you want to

Structural

type Foo = {
    foo: string;
    handleStuff: () => void;
}

type Bar = {
    foo: string;
}

let foo: Foo = { 
    foo: 'foo', 
    handleStuff: () => ({}) 
};

// works, struc. of `Bar` is subset
const bar: Bar = foo;


// won't compile. 
// `handleStuff` is missing
foo = bar;

class Foo {
    public string Foo { get; set; }
}

class Bar {
    public string Foo { get; set; }
}



Foo foo = new Foo { Foo = "foo" };


// won't compile.
// Its different type
Bar bar = foo;
Nominal
#life

let foo = { 
    foo: 'foo', 
    handleStuff: () => ({}) 
};


const bar = 5;

bar = foo;

foo = bar;

Structural type system*

Michał Michalczuk

@michalczukm

Senior Software Engineer

Spartez / Atlassian

Jira Cloud

Modeling data

with
             
​             🤔

You

are responsible

for your models

No matter how 💩 is data

Let's take a look at some problems challenges with our data models

😈

Imagine this data

[
    {
        "id": 1,
        "name": "Fancy product",
        "type": "stock",
        "price": 100,
        "amountInStock": 101
    },
    {
        "id": 2,
        "name": "Ultimate product",
        "type": "marketing",
        "price": 200,
        "marketingName": "GameChanger2000",
        "description": "our new ultimate product!"
    },
    {
        "id": 3,
        "name": "Another product",
        "type": "stock",
        "price": 300,
        "amountInStock": 301
    }
]

Only for "stock"

product

Only for "marketing"

product

Where is the 💩 ?

Write a model

// 😢
type Product = {
  id: string,
  type: 'stock' | 'marketing',
  description?: string, // marketing only
  marketingName?: string, // marketing only
  price: number,
  amountInStock?: number // stock only
};

Problems:

  • 😢 if-ing the optional fields
  • 😢 don't know scenarios when fields are filled

⚡️Improve: Discriminated union type

⚡️Improve: Avoid fields duplication

Imagine - support multiple API versions

// 😢
type Client = {
  id: string,
  name: string,
  version: number,
  WasCreatedOnDate?: Date, // removed in v2
  createdOnUTC?: string, // v2 only
  refId?: string // v2 only
};

Problems:

  • 😢 if-ing the optional fields
  • 😢 don't know when fields are filled

⚡️Quick improve: disc. union type again

⚡️Better way: Type helpers

Doctor, I have too many types 🥵

// this is just internal type, it should never be used outside `Phone`
type PhoneFeatures = {
  isWaterproof: boolean,
  ramAmount: number,
  cpu: string
};

export type Phone = {
  id: string,
  features: PhoneFeatures,
  promotedFeature: 'isWaterproof' | 'ramAmount' | 'cpu'
}

Problems:

  • 😢 types pollution
  • 😢 types can be exposed without their "context"/"purpose"

⚡️ Inline + index type

type Phone = {
  id: string,
  features: {
    isWaterproof: boolean,
    ramAmount: number,
    cpu: string
  },
  promotedFeature: 'isWaterproof' | 'ramAmount' | 'cpu'
}



function handle(features: Phone['features']) {
  features.isWaterproof;
}
(parameter) features: {
    isWaterproof: boolean;
    ramAmount: number;
    cpu: string;
}

💩 

⚡️Index types: Avoid types pollution

⚡️More index types examples

About client... imagine this

[
  // one client row
  [
     { 
       "id": 1 
     }, 
     { 
       "clientId": "100",
       "revenue": 12345
     }, 
     { 
       "country": "Poland",
       "phoneNumber": "000-000-000"
     }
  ],
  ...
]

⚡️What about tuples

What if you..
don't want to add typings for some fields 😱

Why?

  • I'm just passing them through my app/service
  • I only want to show a shape, writing/maintaining those fields types not make sense
  • ... any other GOOD reason 😊
any
  • literally anything
  • can access/call anything
  • can assign anything
unknown
  • literally unknown 😎
  • cannot access/call at all
  • can assign anything
  • Mostly can be avoided
  • Bad practice
  • Your last hope
  • Perfect for only "passing" stuff
  • "Safe" - you won't do anything stupid 

⚡️In `unknown` we trust

class
  • compiles to JS class/proto
  • don't use it for describing data
type
  • compilation time only
  • has to be unique in scope
  • type/data modeling
interface
  • compilation time only
  • interfaces with same name merges
  • use if you want to implement it

Something more?

 

How about this data

[
  {
    "id": 1,
    "name": "Client 1",
    "continent": "AF"
  },
  {
    "id": 2,
    "name": "Client 2",
    "continent": "OC"
  },
  {
    "id": 3,
    "name": "Client 3",
    "continent": "EU"
  }
]

Continent

acronym

We can describe it with type

// 😢
type Client = {
  id: string,
  name: string,
  continent: 'AF' | 'AN' | 'AS' | 'EU' | 'NA' | 'OC' | 'SA';
}

Problems:

  • 😢 no value type, should be enumerated
  • 😢 why not meaningfull names over API values

⚡️Improve: String-based enum

😇 Good enums

😈 Tricky enums

😱

Speaking of enums and types.

 

Where to put types? 🤔

*.d.ts

😰Living on the edge

😰Living on the edge

export enum Continent {
    Africa = 'AF',
    Antarctica = 'AN',
    Asia = 'AS',
    Europe = 'EU',
    NorthAmerica = 'NA',
    Oceania = 'OC',
    SouthAmerica = 'SA'
}
my-types.d.ts
export enum Continent {
    Africa = 'AF',
    Antarctica = 'AN',
    Asia = 'AS',
    Europe = 'EU',
    NorthAmerica = 'NA',
    Oceania = 'OC',
    SouthAmerica = 'SA'
}
my-model.ts
"use strict";
var Continent;
(function (Continent) {
    Continent["Africa"] = "AF";
    Continent["Antarctica"] = "AN";
    Continent["Asia"] = "AS";
    Continent["Europe"] = "EU";
    Continent["NorthAmerica"] = "NA";
    Continent["Oceania"] = "OC";
    Continent["SouthAmerica"] = "SA";
})(Continent || (Continent = {}));
 

compile

compile

😱

😰Living on the edge

declare const PolandISOCode = 'PL';
my-types.d.ts
declare const PolandISOCode = 'PL';
my-model.ts
"use strict";
 

compile

compile

😱

😱

Hey! what about

Algebraic Data Types

Algebraic Data Types

Product

Sum

C(A | B) = C(A) + C(B)
C([A, B]) = C(A) x C(B)
  • tuples
  • records (index)
  • union type

Summary time!

  • use TS rich types toolset
  • type, type and type
  • remember which TS elements have JS dist.
  • avoid types pollution
  • be rational
  • don't make your code look like git conflict

Feedback?

Yes please