Runtime Type Safety in Typescript

@ValentinKononov

@ValentinKononov

Javascript Intro

Where we can use 

 
  • Started in 1995 by Netscape Navigator and SUN
  • Created by Brendan Eich
  • To add interactivity to HTML
  • For designers

«The World’s Most Misunderstood Programming Language Has Become the World’s Most Popular Programming Language»

Javascript Intro

Javascript Intro

> '5' - 3
> '5' + 3
> '5' + - '2'
> 5 + {v: 2}
> 5 - {v: 2}
< 2 		// Number 
< '53'		// String
< '5-2'		// String
< '5[object Object]' // String
< NaN		// NaN Number

Javascript Intro

> typeof "hello world"
< "string"


> typeof new String('hello world')
< "object"
  • Function is object
  • Objects of Different types can behave similarly
  • Prototypes instead of classes
  • Dynamics everywhere
  • No real private members
  • ....

Typescript

  • Introduced in 2012 by MS
  • JS code is valid TS code
  • Object-Oriented
  • Strong-Typed
  • Compilation

Strong or Weak Typed Languages

Typescript

Typescript Creator

Anders Hejlsberg created:

  • Typescript
  • C#
  • Delphi
  • Turbo Pascal

ANYthing

usage of Any in typescript

@ValentinKononov

public ngOnInit(): void {
   const movie = {
       name: 'Game of Thrones',
   };
   this.showToast(movie);
}

private showToast(item: any): void {
   this.toaster.show(`Today's movie is: ${item.name}`);
}
public ngOnInit(): void {
   const movie = {
       title: 'Game of Thrones',
   };
   this.showToast(movie);
}

private showToast(item: any): void {
   this.toaster.show(`Today's movie is: ${item.name}`);
}

'Undefined' in message

ANYthing

use interfaces and classes

interface Movie {
   name: string;
}

public ngOnInit(): void {
   const movie: Movie = {
       name: 'Game of Thrones',
   };
   this.showToast(movie);
}

private showToast(item: Movie): void {
   this.toaster.show(`Today's movie is: ${item.name}`);
}

TS2339: Property 'name' does not exist on type 'Movie'.

interface Movie {
   title: string;
}

public ngOnInit(): void {
   const movie: Movie = {
       name: 'Game of Thrones',
   };
   this.showToast(movie);
}

private showToast(item: Movie): void {
   this.toaster.show(`Today's movie is: ${item.name}`);
}

ANYthing

@ValentinKononov

Typescript Pipeline

  • Write TS code
  • Compilation (transpilation) 
    • static code analysis
    • type checks
    • tests
  • Javascript
  • JS runs in node or browser
  • No types

TS Code Pipeline

Typescript

// Typescript
export class SampleService {
    public multiply(num: number, num2: number): number {
        return  num * num2;
    }
}
// Javascript
"use strict";
Object.defineProperty(exports, "__esModule", 
                      { value: true });
class SampleService {
    multiply(num, num2) {
        return num * num2;
    }
}
exports.SampleService = SampleService;

Sample

How we can trick TS?

const arg1 = 2, arg2 = 3;
const result1 = service.multiply(arg1, arg2);
// result = 6
const arg1 = 2, arg2: any = '3';
const result1 = service.multiply(arg1, arg2);
// result = 6
const arg1 = 2, arg2: any = 'foo';
const result1 = service.multiply(arg1, arg2);
// result = NaN
const arg1 = 2, arg2: any = { name: 'foo' };
const result1 = service.multiply(arg1, arg2);
// result = NaN
const arg1 = 9, arg2: any = '9';
const result1 = service.sum(arg1, arg2);
// result = 99

Potential issues

Who cares?

// usage of ANY
const arg1: any;
function (arg: any) {...}
// usage of literal properties
const obj = getObj();
someFunc(obj['prop']);
  • ANY in code
  • Literal Property Access
  • In Library: any outside calls
  • On Backend: Request from Frontend
  • On Frontend: Response from Backend
  • On Frontend: potential attack, code-breaking
  • Pure Javascript as part of Typescript code
  • Validation of HTTP requests / responses
  • Tests
  • Manual checks
multiplyChecked(num: number, num2: number): number {
  if (typeof num !== 'number') {
	throw Error(`Wrong arg 1: ${typeof num}`)
  }
  if (typeof num2 !== 'number') {
	throw Error(`Wrong arg 2: ${typeof num2}`)
  }
  return  num * num2;
}

Solutions

@ValentinKononov

Easy Solution

@Typed()
public multiplyChecked(num: number, num2: number): number {
    return  num * num2;
}

Decorator

import 'reflect-metadata';
export function Typed() {
  
  return (target: Object, propertyName: string, 
          descriptor: TypedPropertyDescriptor<Function>) => {
    
    const method = descriptor.value;
    descriptor.value = function() {
      
      checkTypes(target, propertyName, arguments);
      
      return method.apply(this, arguments);
    };
    
  };
}

Under the hood

function checkTypes(
  target: Object, propertyName: string, args: any[]): void {
    
    const paramTypes = Reflect.getMetadata(
      'design:paramtypes', target, propertyName);

    paramTypes.forEach((param, index) => {
      const actualType = typeof arguments[index];
      const expectedType = 
        param instanceof Function ? typeof param() : param.name;
      if (actualType !== expectedType) {

        throw new Error(`Argument: ${index} of function ${propertyName} 
                         has type: ${actualType} different from 
                         expected type: ${expectedType}.`);
      }
  });
}

Under the hood

Decorator Details

@ValentinKononov

  • target: Object - an instance of the class, where the function was called
  • propertyName: string - function name
  • descriptor: TypedPropertyDescriptor<Function>  - how we can replace or call initial function

Performance

@ValentinKononov

±0.2 ms 
overhead for type checks

NPM Package

npm install ts-stronger-types --save

@ValentinKononov

Other Options

IO-TS

@ValentinKononov

Runtime type system for IO decoding/encoding

import * as t from 'io-ts'

const User = t.type({
  userId: t.number,
  name: t.string
})
npm install io-ts

Superstruct-TS

@ValentinKononov

Typescript customer transformer for json validation based on type

import { validate } from "superstruct-ts-transformer";

type User = {
  name: string;
  alive: boolean;
};

const obj = validate<User>(
  JSON.parse('{ "name": "Me", "alive": true }'));
npm install superstruct-ts-transformer

Class-Decorator

Validation with TypeScript decorators

export class Post {
    @IsString()
    title: string;

    @IsInt()
    @Min(0)
    @Max(10)
    rating: number;

    @IsEmail()
    email: string;

    @IsDate()
    createDate: Date;
}

validate(obj)
  .then(() => {...})
  .catch(err => {...})
validate(obj)
  .then(() => {...})
  .catch(err => {...})
npm install class-validators --save

Auto Smoke Test

@ValentinKononov

Solution from friends research

  • mark every meaningful function/endpoint with some attribute
  • in tests call EVERY such function
  • SUCCESS if not failed

Conclusion

@ValentinKononov

  • Think about runtime
  • Make code predictable
  • Test your code
  • Use validation when it makes sense
  • Enjoy coding!

Runtime Type Safety in Typescript

By Valentin Kononov

Runtime Type Safety in Typescript

Initial version of slides for topic

  • 287

More from Valentin Kononov