TypeScript type-guards 💂‍♂️

(pre-intro) TypeScript in 10 seconds

// JavaScript

const a = 10;
const myObject = { a: 1, b: 'string' }

function hello_world(name) {
    return `hello ${name}!`;
}
// TypeScript

interface MyObject {
   a: number;
   b: string;
}

const a: number = 10;
const myObject: MyObject = { a: 1, b: 'string' }

function hello_world(name: string) {
    return `hello ${name}!`;
}

"Types aren't real"

Typing "weak spots"



const chat: Chat = await http.post(
  `/chats`,
  { id }
).then(
  response => response.body.chat as Chat
);


// ...

export const Form = reduxForm<any, any>(
  /* ... */
)

What is a type-guards?

A type-guards is an expression that guarantee that a type is valid in a scope

Example: typeof

TypeScript embed type-guards

typeof type-guards

function formatMoney(amount: string | number): string {
   let value = amount; // value type is number or string 
   if (typeof amount === "string") {
      value = parseInt(amount, 10); // amount type is string
   }
   return value + " $"; // value type is number
}

// calling formatMoney({ myObject: 1} as any)
// will not call parseInt with an object

TypeScript embed type-guards

TypeScript and JavaScript runtime are now tied to the same behaviour.

The Discriminated Unions

interface Action { type: string; }
interface ActionA extends Action {
  type: 'ActionA';
  mypropA: string;
}

interface ActionB extends Action {
  type: 'ActionB';
  mypropB: string;
}

type anyAction = ActionA | ActionB;

// ...
function reducer(state: State, action: anyAction): Action {
  switch(action.type) { // "ActionA" | "ActionB"
    case 'ActionA':
      return { ...state, prop: action.mypropB }; // TS ERROR!
      break;
    case 'ActionB':
      return { ...state, prop: action.mypropB }; // OK
      break;
    default:
      return state;
  }
}

The Discriminated Unions

Discriminated Unions is a pattern that allow to build types that shares a common property but have different shapes

  1. Types that have a common, singleton type property — the discriminant.




     
  2. Then, a type alias that takes the union of those types — the union.
     
  3. Finally, a type guard on the common property (on the discriminant).
interface Action {
  type: string; // the discriminant
}

interface ActionA extends Action {
  type: 'ActionA';
  mypropA: string;
}

interface ActionB extends Action {
  type: 'ActionB';
  mypropB: string;
}

type anyAction = ActionA | ActionB; // the union



// ...

switch(action.type) { /* ... */ }

User-defined type-guards

“real world usage” of TypeScript is not restricted to scalar types (string, boolean, number, etc…).
Real world applications mainly deals with complex object or custom types.
 

This is when “User-Defined Type Guards” help us.

User-defined type-guards


function isFish(pet: any): pet is Fish {
    return pet.swim !== undefined;
}
  • guard function argument type, like for overloads, should be as open as possible.
     
  • a new is operator, called type predicate.

User-defined type-guards

interface Fish {
    swim: (number, number, number) => [number, number, number]
}

interface Cat {
    walk: (number, number) => [number, number]
}

const isFish : (pet: any) => pet is Fish = pet => !!pet.swim;


const f: Fish = { /* ... */ }

if (isFish(f)) {
    // true for TypeScript, and true at runtime
}

User-defined type-guards

Good points of User-Defined type-guards:

  • matches real-world expectations
    • more flexible
    • support complex types
  • stateless and isolated ➡️ testable

Where to start?

Avoid "any" and "as" and use type-guards for complex use-cases

if (!!myobject.someProp) {
  (<MyType>myobject).someMethod()
}

➡️  User-Defined type-guards

function reducer(state: State, action: any): Action {
  switch(action.type) { // "ActionA" | "ActionB"
    case 'ActionA':
      return { ...state, prop: <ActionA>action.mypropA };
      break;
    // ...
    default:
      return state;
  }
}

➡️  Discriminated Unions

Where to start?

Save time while building type-guards by using io-ts

Introduced in  “Typescript and validations at runtime boundaries” article by @lorefnon,
io-ts is an active library that aim to solve the same problem:

TypeScript compatible runtime type system for IO decoding/encoding

Where to start?

io-ts overview

const Person = t.interface({
  name: t.string,
  age: t.string  
})


interface IPerson extends t.TypeOf<typeof Person> {}

// same as
//   interface IPerson {
//     name: string
//     age: number
//   }


let a: any = {};

if (Person.is(a)) {
  // a is a Person type
}

Powerful API:
 

- decode()

- encode()

- is()

 

and also,

- custom error reporters
- unions and recursives types support

Conclusion

  • Types can be real
  • Avoid "as" operator, use type-guards
  • TypeScript is more powerful than you think

Thanks for listening!

TypeScript - type-guards (10 min version)

By Charly Poly

TypeScript - type-guards (10 min version)

  • 1,164