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
- Types that have a common, singleton type property — the discriminant.
 - Then, a type alias that takes the union of those types — the union.
 - 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!
For more, look at the "TypeScript — Make types “real”, the type guards" article
TypeScript - type-guards (10 min version)
By Charly Poly
TypeScript - type-guards (10 min version)
- 1,282