LET THE TYPE SYSTEM
BE YOUR FRIEND
ABOUT ME
👤 Wiktor Toporek
💼 PHP → Frontend → Node.js
👍 Functional programming
❤ Elm
🐦 Twitter: @vViktorPL
function sum(elements) {
  return elements.reduce((a, b) => a + b, 0)
}
function sum(elements) {
  return elements.reduce((a, b) => a + b, 0)
}
function sum(elements) {
  return elements.reduce((a, b) => a + b, 0)
}
function sum(elements) {
  return elements.reduce((a, b) => a + b, 0)
}
function sum(elements) {
  return elements.reduce((a, b) => a + b, 0)
}
HOW TO DEAL WITH IT?
HOW TO DEAL WITH IT?
1. TRUST DEVELOPERS
HOW TO DEAL WITH IT?
1. TRUST DEVELOPERS
2. TRY TO DEFEND
function sum(elements) {
  if (!Array.isArray(elements)) {
    throw 'Elements argument should be an array'
  }
  if (elements.some(
    element => typeof element !== 'number'
  )) {
    throw 'All elements should be a number'
  }
  return elements.reduce((a, b) => a + b, 0)
}
function sum(elements) {
  if (!Array.isArray(elements)) {
    throw 'Elements argument should be an array'
  }
  if (elements.some(
    element => typeof element !== 'number'
  )) {
    throw 'All elements should be a number'
  }
  return elements.reduce((a, b) => a + b, 0)
}
function sum(elements) {
  if (!Array.isArray(elements)) {
    throw 'Elements argument should be an array'
  }
  if (elements.some(
    element => typeof element !== 'number'
  )) {
    throw 'All elements should be a number'
  }
  return elements.reduce((a, b) => a + b, 0)
}
✅ Function won’t be used improperly
function sum(elements) {
  if (!Array.isArray(elements)) {
    throw 'Elements argument should be an array'
  }
  if (elements.some(
    element => typeof element !== 'number'
  )) {
    throw 'All elements should be a number'
  }
  return elements.reduce((a, b) => a + b, 0)
}
✅ Function won’t be used improperly
✅ Exception will hint a dev
function sum(elements) {
  if (!Array.isArray(elements)) {
    throw 'Elements argument should be an array'
  }
  if (elements.some(
    element => typeof element !== 'number'
  )) {
    throw 'All elements should be a number'
  }
  return elements.reduce((a, b) => a + b, 0)
}
✅ Function won’t be used improperly
✅ Exception will hint a dev
👎 Problem won’t show until runtime
function sum(elements) {
  if (!Array.isArray(elements)) {
    throw 'Elements argument should be an array'
  }
  if (elements.some(
    element => typeof element !== 'number'
  )) {
    throw 'All elements should be a number'
  }
  return elements.reduce((a, b) => a + b, 0)
}
✅ Function won’t be used improperly
✅ Exception will hint a dev
👎 Problem won’t show until runtime
👎 Extra code obfuscates the essence
function sum(elements) {
  if (!Array.isArray(elements)) {
    throw 'Elements argument should be an array'
  }
  if (elements.some(
    element => typeof element !== 'number'
  )) {
    throw 'All elements should be a number'
  }
  return elements.reduce((a, b) => a + b, 0)
}
✅ Function won’t be used improperly
✅ Exception will hint a dev
👎 Problem won’t show until runtime
👎 Extra code obfuscates the essence
👎 Many edge-cases to consider
function sum(elements) {
  if (!Array.isArray(elements)) {
    throw 'Elements argument should be an array'
  }
  if (elements.some(
    element => typeof element !== 'number'
  )) {
    throw 'All elements should be a number'
  }
  return elements.reduce((a, b) => a + b, 0)
}
✅ Function won’t be used improperly
✅ Exception will hint a dev
👎 Problem won’t show until runtime
👎 Extra code obfuscates the essence
👎 Many edge-cases to consider
👎 More tests to write
function avg(elements) {
  return sum(elements) / elements.length
}
function avg(elements) {
  if (elements.length === 0) {
    throw 'Cannot calculate average for empty array'
  }
  return sum(elements) / elements.length
}
function avg(elements) {
  if (!Array.isArray(elements)) {
    throw 'Elements argument should be an array'
  }
  if (elements.length === 0) {
    throw 'Cannot calculate average for empty array'
  }
  return sum(elements) / elements.length
}
function avg(elements) {
  if (!Array.isArray(elements)) {
    throw 'Elements argument should be an array'
  }
  if (elements.length === 0) {
    throw 'Cannot calculate average for empty array'
  }
  return sum(elements) / elements.length
}
👎 Redundant validation
HOW TO DEAL WITH IT?
HOW TO DEAL WITH IT?
1. TRUST DEVELOPERS
HOW TO DEAL WITH IT?
1. TRUST DEVELOPERS
2. TRY TO DEFEND
HOW TO DEAL WITH IT?
1. TRUST DEVELOPERS
2. TRY TO DEFEND
3. EMPLOY A BODY GUARD
function sum(elements: number[]) {
  return elements.reduce((a, b) => a + b, 0);
}
function avg(elements: number[]) {
  if (elements.length === 0) {
    throw Error('Cannot calculate average for empty array');
  }
  return sum(elements) / elements.length;
}
avg(['x', 'y']); // compile error
sum(['x', 'y']); // compile error
avg([1, 2]); // ok
sum([1, 2]); // ok
function sum(elements: number[]) {
  return elements.reduce((a, b) => a + b, 0); 
}
function avg(elements: [number, ...number[]]) {
  return sum(elements) / elements.length;
}
avg([]);     // compile error
avg([1]);    // ok
avg([1, 2]); // ok
function sum(elements: number[]) {
  return elements.reduce((a, b) => a + b, 0); 
}
function avg(elements: [number, ...number[]]) {
  return sum(elements) / elements.length;
}
avg([]);     // compile error
avg([1]);    // ok
avg([1, 2]); // ok
type NonEmptyArray<T> = [T, ...T[]]
function sum(elements: number[]) {
  return elements.reduce((a, b) => a + b, 0); 
}
function avg(elements: [number, ...number[]]) {
  return sum(elements) / elements.length;
}
avg([]);     // compile error
avg([1]);    // ok
avg([1, 2]); // ok
type NonEmptyArray<T> = [T, ...T[]]
function avg(elements: NonEmptyArray<number>) {
  return sum(elements) / elements.length;
}
avg([]);     // compile error
avg([1]);    // ok
avg([1, 2]); // ok
HOWEVER…
HOWEVER…
let x: any = 'foo'
console.log(x.anythingIsPossible())
HOWEVER…
ANY
let x: any = 'foo'
console.log(x.anythingIsPossible())
HOWEVER…
ANY
let x: any = 'foo'
console.log(x.anythingIsPossible())
ANY
BACKEND
BACKEND
[{
  "_id": {
    "$oid": "5968dd23fc13ae04d9000001"
  },
  "product_name": "sildenafil citrate",
  "supplier": "Wisozk Inc",
  "quantity": 261,
  "unit_cost": "$10.47"
}, {
  "_id": {
    "$oid": "5968dd23fc13ae04d9000002"
  },
  "product_name": "Mountain Juniperus ashei",
  "supplier": "Keebler-Hilpert",
  "quantity": 292,
  "unit_cost": "$8.74"
}, {
  "_id": {
    "$oid": "5968dd23fc13ae04d9000003"
  },
  "product_name": "Dextromathorphan HBr",
  "supplier": "Schmitt-Weissnat",
  "quantity": 211,
BACKEND
[{
  "_id": {
    "$oid": "5968dd23fc13ae04d9000001"
  },
  "product_name": "sildenafil citrate",
  "supplier": "Wisozk Inc",
  "quantity": 261,
  "unit_cost": "$10.47"
}, {
  "_id": {
    "$oid": "5968dd23fc13ae04d9000002"
  },
  "product_name": "Mountain Juniperus ashei",
  "supplier": "Keebler-Hilpert",
  "quantity": 292,
  "unit_cost": "$8.74"
}, {
  "_id": {
    "$oid": "5968dd23fc13ae04d9000003"
  },
  "product_name": "Dextromathorphan HBr",
  "supplier": "Schmitt-Weissnat",
  "quantity": 211,
ANY
ANY
UNKNOWN
ANY vs UNKNOWN
let value: any;
let value1: unknown = value;   // OK
let value2: any = value;       // OK
let value3: boolean = value;   // OK
let value4: number = value;    // OK
let value5: string = value;    // OK
let value6: object = value;    // OK
let value7: any[] = value;     // OK
let value8: Function = value;  // OK
ANY vs UNKNOWN
let value: any;
let value1: unknown = value;   // OK
let value2: any = value;       // OK
let value3: boolean = value;   // OK
let value4: number = value;    // OK
let value5: string = value;    // OK
let value6: object = value;    // OK
let value7: any[] = value;     // OK
let value8: Function = value;  // OK
let value: unknown;
let value1: unknown = value;   // OK
let value2: any = value;       // OK
let value3: boolean = value;   // Error
let value4: number = value;    // Error
let value5: string = value;    // Error
let value6: object = value;    // Error
let value7: any[] = value;     // Error
let value8: Function = value;  // Error
ANY vs UNKNOWN
const value = JSON.parse('123')
console.log(value.toUpperCase())
ANY vs UNKNOWN
const value: unknown = JSON.parse('123')
console.log(value.toUpperCase())
ANY vs UNKNOWN
const value: unknown = JSON.parse('123')
console.log(value.toUpperCase())
ANY vs UNKNOWN
const value: unknown = JSON.parse('123')
if (typeof value === 'string') {
  console.log(value.toUpperCase())
}
ANY vs UNKNOWN
VALIDATION
VALIDATION
VALIDATION - DECODERS
DECODERS
DECODERS
VALIDATION
DECODERS
VALIDATION +
DECODERS
VALIDATION
OPTIMAL
STRUCTURE
EXTRACTION
+
VALIDATION - DECODERS
VALIDATION - DECODERS
VALIDATION - DECODERS
ARCHITECTURE
ARCHITECTURE
🗿

STATICALLY TYPED APPLICATION
ARCHITECTURE
🗿

STATICALLY TYPED APPLICATION
😈 BACKEND
ARCHITECTURE
🗿

STATICALLY TYPED APPLICATION
😈 BACKEND
😈 USER INPUT
ARCHITECTURE
🗿

STATICALLY TYPED APPLICATION
😈 BACKEND
😈 USER INPUT
🛂

VALIDATE
ARCHITECTURE
🗿

STATICALLY TYPED APPLICATION
😈 BACKEND
😈 USER INPUT
🛂

VALIDATE
🏷

TAG TYPE
ARCHITECTURE
🗿

STATICALLY TYPED APPLICATION
😈 BACKEND
😈 USER INPUT
🛂

VALIDATE
🏷

TAG TYPE
ARCHITECTURE
🗿

STATICALLY TYPED APPLICATION
😈 BACKEND
😈 USER INPUT
🛂

VALIDATE
🏷

TAG TYPE
{
ARCHITECTURE
🗿

STATICALLY TYPED APPLICATION
😈 BACKEND
😈 USER INPUT
🛂

VALIDATE
🏷

TAG TYPE
{Decoder
STATE OPTIMISATION
EXAMPLE 1
ARRAY | UNDEFINED
type Author = {
  name: string,
  books?: Book[],
}
type Book = {
  title: string,
}
render() {
  const author = this.props.author
  return (
    <div>
      <p>{author.name}</p>
      <ul>
        {author.books && author.books.map(
  ({ title }) => <li>{title}</li>
)}
      </ul>
    </div>
  )
}
ARRAY | UNDEFINED
type Author = {
  name: string,
  books: Book[],
}
type Book = {
  title: string,
}
render() {
  const author = this.props.author
  return (
    <div>
      <p>{author.name}</p>
      <ul>
        {author.books.map(
  ({ title }) => <li>{title}</li>
)}
      </ul>
    </div>
  )
}
EXAMPLE 2
SURVEY
type Survey = {
  questions: Question[],
}
type Question = {
  prompt: string,
  response?: string,
}
type Survey = {
  questions: Question[],
  currentIndex: number,
}
type Question = {
  prompt: string,
  response?: string,
}
REQUIREMENT: QUESTIONS NAVIGATION (FORWARD/BACK)
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?' },
    { prompt: 'Czym się zajmujesz?' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentIndex: 0,
}
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?' },
    { prompt: 'Czym się zajmujesz?' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentIndex: 0,
}
✅
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' },
    { prompt: 'Czym się zajmujesz?' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentIndex: 1,
}
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' },
    { prompt: 'Czym się zajmujesz?' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentIndex: 1,
}
✅
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' },
    { prompt: 'Czym się zajmujesz?', response: 'Frontend' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentIndex: 1,
}
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' },
    { prompt: 'Czym się zajmujesz?', response: 'Frontend' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentIndex: 1,
}
✅
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' },
    { prompt: 'Czym się zajmujesz?', response: 'Frontend' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentIndex: 999,
}
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' },
    { prompt: 'Czym się zajmujesz?', response: 'Frontend' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentIndex: 999,
}
❌
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' },
    { prompt: 'Czym się zajmujesz?', response: 'Frontend' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentIndex: -1,
}
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' },
    { prompt: 'Czym się zajmujesz?', response: 'Frontend' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentIndex: -1,
}
❌
type Survey = {
  questions: Question[],
  currentQuestion: Question,
}
type Question = {
  prompt: string,
  response?: string,
}
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?' },
    { prompt: 'Czym się zajmujesz?' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentQuestion: this.questions[0],
}
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?' },
    { prompt: 'Czym się zajmujesz?' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentQuestion: this.questions[0],
}
✅
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?' },
    { prompt: 'Czym się zajmujesz?' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentQuestion: { prompt: 'No elo' },
}
{
  questions: [
    { prompt: 'Lubisz Uszanowanko?' },
    { prompt: 'Czym się zajmujesz?' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
  currentQuestion: { prompt: 'No elo' },
}
❌
{
  questions: [],
  currentQuestion: { prompt: 'No elo' },
}
NEW REQUIREMENT: AT LEAST ONE QUESTION
{
  questions: [],
  currentQuestion: { prompt: 'No elo' },
}
ZIP LIST
type Survey = {
  previous: Question[],
  currentQuestion: Question,
  remaining: Question[]
}
type Question = {
  prompt: string,
  response?: string,
}
{
  previous: [],
  currentQuestion: { prompt: 'Lubisz Uszanowanko?' },
  remaining: [
    { prompt: 'Czym się zajmujesz?' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
}
{
  previous: [],
  currentQuestion: { prompt: 'Lubisz Uszanowanko?' },
  remaining: [
    { prompt: 'Czym się zajmujesz?' },
    { prompt: 'Nutella z masłem czy bez?' },
  ],
}
✅
{
  previous: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' }
  ],
  currentQuestion: { prompt: 'Czym się zajmujesz?' },
  remaining: [
    { prompt: 'Nutella z masłem czy bez?' },
  ],
}
{
  previous: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' }
  ],
  currentQuestion: { prompt: 'Czym się zajmujesz?' },
  remaining: [
    { prompt: 'Nutella z masłem czy bez?' },
  ],
}
✅
{
  previous: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' },
    { prompt: 'Czym się zajmujesz?', response: 'Frontend' },
  ],
  currentQuestion: { prompt: 'Nutella z masłem czy bez?' },
  remaining: [],
}
{
  previous: [
    { prompt: 'Lubisz Uszanowanko?', response: 'Tak' },
    { prompt: 'Czym się zajmujesz?', response: 'Frontend' },
  ],
  currentQuestion: { prompt: 'Nutella z masłem czy bez?' },
  remaining: [],
}
✅
„
„
— Richard Feldman
MAKE IMPOSSIBLE STATES
IMPOSSIBLE
https://www.youtube.com/watch?v=IcgmSRJHu_8
SUMMARY
SUMMARY
YOUR EFFORT
SUMMARY
YOUR EFFORT
SUMMARY
🗿 Static types 1st, edge-cases 2nd
YOUR EFFORT
SUMMARY
🗿 Static types 1st, edge-cases 2nd
🛂 Validate data before you let it in
YOUR EFFORT
SUMMARY
🗿 Static types 1st, edge-cases 2nd
🛂 Validate data before you let it in
👌 Turn the validated data to optimal type
YOUR EFFORT
SUMMARY
🗿 Static types 1st, edge-cases 2nd
🛂 Validate data before you let it in
👌 Turn the validated data to optimal type
❓ Try "unknown" instead of "any"
YOUR EFFORT
SUMMARY
🗿 Static types 1st, edge-cases 2nd
🛂 Validate data before you let it in
👌 Turn the validated data to optimal type
❓ Try "unknown" instead of "any"
❌ Avoid undefined/null if you can
YOUR EFFORT
SUMMARY
🗿 Static types 1st, edge-cases 2nd
🛂 Validate data before you let it in
👌 Turn the validated data to optimal type
❓ Try "unknown" instead of "any"
❌ Avoid undefined/null if you can
💡 Take some inspiration from statically typed languages like Elm or Haskell
WHAT YOU GET IN
RETURN?
SUMMARY
WHAT YOU GET IN RETURN?
LESS TEST TO WRITE
SUMMARY
WHAT YOU GET IN RETURN?
LESS TEST TO WRITE
SUMMARY
WHAT YOU GET IN RETURN?
APP WILL IDENTIFY PROBLEMS NOT ON IT’S SIDE
SUMMARY
WHAT YOU GET IN RETURN?
APP WILL IDENTIFY PROBLEMS NOT ON IT’S SIDE
SUMMARY
WHAT YOU GET IN RETURN?
MORE CONFIDENCE
SUMMARY
WHAT YOU GET IN RETURN?
MORE CONFIDENCE
LINKS
• https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/
• https://guide.elm-lang.org/effects/json.html#json-decoders
• https://www.youtube.com/watch?v=IcgmSRJHu_8
• https://stackoverflow.com/questions/40794368/what-is-an-opaque-type-in-elm-and-why-is-it-valuable
• https://basarat.gitbooks.io/typescript/docs/tips/nominalTyping.html
• https://www.youtube.com/watch?v=WnTw0z7rD3E
🔗
BOOK RECOMMENDATIONS
📚
BOOK RECOMMENDATIONS
📚
BOOK RECOMMENDATIONS
📚
BOOK RECOMMENDATIONS
📚
THANK YOU

Let the type system be your friend