Runtime Type Safety in Typescript

https://github.com/valentinkononov

http://kononov.space/

 

@ValentinKononov

 

Developer, Speaker

 

 

  1. Why do you work with TypeScript, not pure JavaScript?
  2. What other languages do you use?
  3. What makes you feel safe with your code?

Intro Questions

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

History dive in

  • Founded in 1995 by Netscape Navigator and SUN
  • Written by Brendan Eich
  • Easy to learn the language
  • Adding interactivity to HTML
  • For designers
> '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

JS Tricks

TypeScript

  • Founded in 2012 by Microsoft
  • JS code is valid TS code
  • Object-Oriented
  • Strongly Typed and Static
  • Compilation step

Perks of TypeScript

Anders Hejlsberg created:

  1. TypeScript
  2. C#
  3. Delphi
  4. Turbo / Borland Pascal

TypeScript Creator

@ValentinKononov

ANY

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

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

Hack TS

@ValentinKononov

--noImplicitAny

no-explicit-any

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']`);
}

Literal Access

interface Movie {
   name: string;
   genre: string;
}


getMovieProp(key: keyof Movie, movie: Movie): string {
	return movie[key]?.toString();
}


private showToast(item: Movie): void {
   this.toaster.show(getMoviewProp('name'));
}

TS2345: Argument of type 'name??' is not assignable to parameter of type '"name"|"genre"'

export function* iterableObj() {
    yield 1;
    yield 'am';
    yield 'iterable.';
}

Yield

export function* iterableObj(): 
	Generator<number, void, void> {
  
    yield 1;
    yield 'am';
    yield 'iterable.';
}

Yield

Use TypeScript version >= 3.6

import { select } from 'redux-saga/effects';


// returns ANY object, no checks
const state_0 = yield select(getTaskStatus);

// if Type is specified - TS relyes on it
const state_1: TaskStatus = yield select(getTaskStatus);

// but Type can be specified incorrectly
const state_2: TaskStatus = yield select(getTaskAssignee);

Dependencies

import { select } from 'redux-saga/effects';


// returns ANY object, no checks
export function select
  	<Fn extends (state: any, ...args: any[]) => any>(
  selector: Fn,
  ...args: Tail<Parameters<Fn>>
): SelectEffect

Dependencies

Some Libraries can return ANY

// pathOr from Ramda library

export function pathOr<T>(
	defaultValue: T, path: Path, obj: any): T;

Dependencies

const role = pathOr(null, ['auth', 'user', 'role'], state);

// no compilation errors
const role = pathOr(null, ['auth', 'userrrr', 'role'], state);
export function pathOr<T1, T2, T3, R>(
    defaultValue: R, 
    path: [keyof T1, keyof T2, keyof T3], 
    obj: T1): R;
  • ANY
  • Literal Property Access
  • Pure JavaScript
  • Pure yield usage

Summary of breaks

  • Outer code calls your functions
  • Dependencies
  • Backend / Frontend calls
// 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;

Root cause

Solutions?

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;
}

Validate it

  • Validation of requests / responses
  • Test whatever you can
  • Add checks for important cases

@ValentinKononov

Can it be so simple that even a dog can use it?

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

Decorator

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

import 'reflect-metadata';

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

__decorate([
    src_1.Typed(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Number, Number]),
    __metadata("design:returntype", Number)
], TypedTestService.prototype, "multiplyChecked", null);

In JavaScript

@Typed()
public multiplyChecked(num: number, num2: number): number {
    return  num * num2;
}
npm install ts-stronger-types --save

NPM Package

@ValentinKononov

Other Options

@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

IO-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

Superstruct-TS

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

Class-Decorator

@ValentinKononov

@ValentinKononov

Conclusion

  • Think about runtime
  • Make code predictable
  • Test your code
  • Use types and validation
  • Enjoy well written code!
Made with Slides.com