Runtime Type Safety in Typescript

https://github.com/valentinkononov

http://kononov.space/

 

@ValentinKononov

 

Developer, Speaker

 

 

  1. Write code in JavaScript?
  2. Write code in TypeScript
  3. What other languages do you use?
  4. What do you miss in JavaScript?
  5. What makes you happy when writing code?

Start with Questions

History dive in

«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

Stackoverflow research from 2019

Numbers

> '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 Jests

> typeof "hello world"
< "string"


> typeof new String('hello world')
< "object"

JavaScript Fundamentals

  • Function is object
  • Object Type !== Object Behavior
  • Prototypes
  • Dynamics everywhere
  • No real privacy

TypeScript

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

Perks of TypeScript

Typescript

Strong or Weak?

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

@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 as string]?.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"'

@ValentinKononov

TypeScript Pipeline

TypeScript Pipeline

  • Write TS code
  • Compile (transpile)
    • static code analysis
    • static type checks
    • linters
  • JavaScript is ready
  • JavaScript runs in node / browser
    • no types in JavaScript
// 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;

Compilation Errors

So what?

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

// usage of ANY
const arg1: any;
function (arg: any) {...}
// usage of literal properties
const obj = getObj();
someFunc(obj['prop']);
  • ANY
  • Literal Property Access
  • Pure JavaScript
  • Outside calls for libraries
  • Backend: requests from clients
  • Frontend: unexpected response
  • Frontend: attacks, code breaking

Who cares?

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
  • Manual Checks

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

@ValentinKononov

Decorator Basics

  • target - Object, instance which function was called
  • propertyName - String, function name
  • descriptor - TypedPropertyDescriptor<Function>, how we can access initial function

@ValentinKononov

Performance

±0.2 ms

overhead for type checks

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

Auto Smoke Test

Suggestion from friends research

  • mark every important function / endpoint with custom decorator
  • in tests call EVERY such function with random arguments
  • tests SUCCEED if all functions have not failed

@ValentinKononov

@ValentinKononov

Conclusion

  • Think about runtime
  • Make code predictable
  • Test your code
  • Use types and validation
  • Enjoy well written code!

Runtime Type Safety in Typescript for jsPoland

By Valentin Kononov

Runtime Type Safety in Typescript for jsPoland

Slides for conference topic Runtime Type Safety in Typescript. This practical and also philosophical talk about how to make work with TS safer if this is possible at all.

  • 330