These slides are published.
Feel free to follow along.
I like a presentation that tells you exactly what it's going to be.
io-ts
Who is well-versed in
?
Who is well-versed in
?
Who knows what a
Type Guard is?
Who could explain the difference between static and dynamic typing?
No, I'm not going to ask you to do it.
Who knows some type theory?
JavaScript“Sure, why not, add an array to a string.”
// https://github.com/denysdovhan/wtfjs
parseInt("Infinity", 19); // -> 18
parseInt(0.000001); // -> 0
parseInt(0.0000001); // -> 1
parseInt(1 / 1999999); // -> 5
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'
[] == 0; // -> true
[undefined] == ''; // -> true
[0] == ''; // -> false
const netId = 'sdc2637';
const netIdObj = await getUserObject(netId);
const newEmplId = '3027689';
netId['emplId'] = newEmplId;
console.log(netId['emplId']); // prints "undefined"
One might argue that that's the fault of bad programming habits...
...but one also ought to consider Murphy's Law.
TypeScript
Are you SURE that number isn't undefined?
parseInt(0.000001);
// Argument of type 'number' is not assignable to
// parameter of type 'string'.ts(2345)
[1, 2, 3] + [4, 5, 6];
// Operator '+' cannot be applied to types 'number[]' and
// 'number[]'.ts(2365)
[] == 0;
// This comparison appears to be unintentional because the
// types 'never[]' and 'number' have no overlap.ts(2367)
These are all still syntactically correct.
TypeScript checks more than syntax.
const netId = 'sdc2637';
const netIdObj = await getUserObject(netId);
const newEmplId = '3027689';
netId['emplId'] = newEmplId;
// Element implicitly has an 'any' type because
// index expression is not of type 'number'.ts(7015)
console.log(netId['emplId']);
It's not the most helpful error message in the world…
…but it's better than no error at all.
type NetID = {
netId: string
emplId: string
displayName: string
}
async function getUserObject(netId: string): Promise<NetID> {
}
const netId = 'sdc2637'; // type: string
const netIdObj = await getUserObject(netId); // type: NetID
type NetID = {
netId: string
emplId: string
displayName: string
}
async function getUserObject(netId: string): Promise<NetID> {
return {
netId: 'sdc2637',
emplId: '3027689',
displayName: 'Spencer Colton'
};
}
const netId = 'sdc2637'; // type: string
const netIdObj = await getUserObject(netId); // type: NetID
type NetID = {
netId: string
emplId: string
displayName: string
}
async function getUserObject(netId: string): Promise<NetID> {
const userResponse = await fetch(`https://myawesomenetidapi.edu/${netId}`);
return await userResponse.json();
}
const netId = 'sdc2637'; // type: string
const netIdObj = await getUserObject(netId); // type: NetID
type NetID = {
netId: string
emplId: string
displayName: string
}
async function getUserObject(netId: string): Promise<NetID> {
const userResponse = await fetch(`https://myawesomenetidapi.edu/${netId}`);
return await userResponse.json();
// This works, but should it?
// All of this is checked at compile-time, and the compiler cannot
// make a guarantee about what this API returns!
}
const netId = 'sdc2637'; // type: string
const netIdObj = await getUserObject(netId); // type: NetID
JavaScript provides some basic runtime type checking that TypeScript can make use of.
function getTheLengthOfAThing(aThing: unknown): number {
return aThing.length;
}
//
function getTheLengthOfAThing(aThing: unknown): number {
return aThing.length;
}
// 'aString' is of type 'unknown'.ts(18046)
function getTheLengthOfAThing(aThing: unknown): number {
if (typeof(aThing) !== 'string')
throw new Error('I only know string things');
return aThing.length;
}
// Works!
Fabulous! Certainly it's always concise and readable, right?
// For information only
type ThingWithAName = {
name: string
}
function getTheNameOfTheUnknownThing(aThing: unknown): string {
if (typeof(aThing) !== 'object' || aThing === null)
throw new Error('Only objects have names');
if (!('name' in aThing))
throw new Error('Thing has no name');
const name = aThing.name;
if (typeof(name) !== 'string')
throw new Error('Name is not a string');
return name;
}
// For information only
type ThingWithAName = {
name: string
}
function getTheNameOfTheUnknownThing(aThing: unknown): string {
if (typeof(aThing) !== 'object' || aThing === null)
throw new Error('Only objects have names');
if (!('name' in aThing))
throw new Error('Thing has no name');
const name = aThing.name;
if (typeof(name) !== 'string')
throw new Error('Name is not a string');
return name;
}
// For information only
type ThingWithAName = {
name: string
}
function getTheNameOfTheUnknownThing(aThing: unknown): string {
if (typeof(aThing) !== 'object' || aThing === null)
throw new Error('Only objects have names');
if (!('name' in aThing))
throw new Error('Thing has no name');
const name = aThing.name;
if (typeof(name) !== 'string')
throw new Error('Name is not a string');
return name;
}
// For information only
type ThingWithAName = {
name: string
}
function getTheNameOfTheUnknownThing(aThing: unknown): string {
if (typeof(aThing) !== 'object' || aThing === null)
throw new Error('Only objects have names');
if (!('name' in aThing))
throw new Error('Thing has no name');
const name = aThing.name;
if (typeof(name) !== 'string')
throw new Error('Name is not a string');
return name;
}
// For information only
type ThingWithAName = {
name: string
}
function getTheNameOfTheUnknownThing(aThing: unknown): string {
if (typeof(aThing) !== 'object' || aThing === null)
throw new Error('Only objects have names');
if (!('name' in aThing))
throw new Error('Thing has no name');
const name = aThing.name;
if (typeof(name) !== 'string')
throw new Error('Name is not a string');
return name;
}
But it gets worse...
type ThingWithAName {
name: string
}
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
const thing: unknown = { name: 'Unknown Thing' };
if (typeof(thing) !== 'object' || thing === null ||
!('name' in thing) || typeof(thing.name) !== 'string')
throw new Error('Thing does not have a name');
console.log(getTheNameOfTheThing(thing));
//
type ThingWithAName {
name: string
}
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
const thing: unknown = { name: 'Unknown Thing' };
if (typeof(thing) !== 'object' || thing === null ||
!('name' in thing) || typeof(thing.name) !== 'string')
throw new Error('Thing does not have a name');
console.log(getTheNameOfTheThing(thing));
// Type 'unknown' is not assignable to type 'string'.ts(2345)
This is a deliberate and fundamental limitation of the language.
https://github.com/microsoft/TypeScript/issues/31755#issuecomment-498669080
(out of scope)
type ThingWithAName {
name: string
}
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
const thing: unknown = { name: 'Unknown Thing' };
console.log(getTheNameOfTheThing(thing as ThingWithAName));
type ThingWithAName {
name: string
}
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
const thing: unknown = { name: 'Unknown Thing' };
console.log(getTheNameOfTheThing(thing as ThingWithAName));
Generally considered bad form.
You've given up a lot that was useful about the
compiler.
Bugs that
was built to prevent now come back.
type ThingWithAName {
name: string
}
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
function thingHasAName(aThing: unknown): aThing is ThingWithAName {
return typeof(aThing) === 'object' && aThing !== null &&
'name' in aThing && typeof(aThing.name) === 'string';
}
const thing: unknown = { name: 'Unknown Thing' };
if (!thingHasAName(thing))
throw new Error('Thing does not have a name');
console.log(getTheNameOfTheThing(thing));
type ThingWithAName {
name: string
}
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
function thingHasAName(aThing: unknown): aThing is ThingWithAName {
return typeof(aThing) === 'object' && aThing !== null &&
'name' in aThing && typeof(aThing.name) === 'string';
}
const thing: unknown = { name: 'Unknown Thing' };
if (!thingHasAName(thing))
throw new Error('Thing does not have a name');
console.log(getTheNameOfTheThing(thing));
type ThingWithAName {
name: string
}
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
function thingHasAName(aThing: unknown): aThing is ThingWithAName {
return typeof(aThing) === 'object' && aThing !== null &&
'name' in aThing && typeof(aThing.name) === 'string';
}
const thing: unknown = { name: 'Unknown Thing' };
if (!thingHasAName(thing))
throw new Error('Thing does not have a name');
console.log(getTheNameOfTheThing(thing));
Cool, but can I lie?
Of course.
type ThingWithAName {
name: string
}
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
function thingHasAName(aThing: unknown): aThing is ThingWithAName {
return true;
}
const thing: unknown = 'this string has no name';
if (!thingHasAName(thing))
throw new Error('Thing does not have a name');
console.log(getTheNameOfTheThing(thing)); // prints "undefined"
type NetID = {
netId: string,
active: boolean,
hrEmplId: string,
sesEmplId: string,
displayName: string,
firstName: string,
middleName: string,
lastName: string,
pwdLastChangeDate: string,
nuAllSchoolAffiliations: string[]
...
}
function isNetID(thing: unknown): thing is NetID {
if (typeof(thing) !== 'object' || thing === null)
return false;
if (!('netId' in thing && 'active' in thing && 'hrEmplId' in thing &&
'sesEmplId' in thing && 'displayName' in thing && 'firstName' in thing &&
'middleName' in thing && 'lastName' in thing && 'pwdLastChangeDate' in thing &&
'nuAllSchoolAffiliations' in thing))
return false;
if (typeof(thing.netId) !== 'string' || typeof(thing.active) !== 'boolean' ||
typeof(thing.hrEmplId !== 'string') || typeof(thing.sesEmplId) !== 'string' ||
typeof(thing.displayName) !== 'string' || typeof(thing.firstName) !== 'string' ||
typeof(thing.middleName) !== 'string' || typeof(thing.lastName) !== 'string' ||
typeof(thing.pwdLastChangeDate) !== 'string' || typeof(thing.nuAllSchoolAffiliations !== 'string'))
return false;
if (!Array.isArray(thing.nuAllSchoolAffiliations))
return false;
return true;
}
Shouldn't there be a library for this?
io-ts exploits the patterns in writing these type guards.
If you know how to tell if something is a string, you can reuse that code when checking if
a property of an object is a string.
And so on, and so forth...
import * as t from 'io-ts';
const ThingWithAName = t.type({
name: t.string
});
type ThingWithAName = t.typeof<typeof ThingWithAName>
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
const thing: unknown = { name: 'Name' };
if (!ThingWithAName.is(thing))
throw new Error('Thing does not have a name');
console.log(getTheNameOfTheThing(thing)); // Works!
import * as t from 'io-ts';
const ThingWithAName = t.type({
name: t.string
});
type ThingWithAName = t.TypeOf<typeof ThingWithAName>
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
const thing: unknown = { name: 'Name' };
if (!ThingWithAName.is(thing))
throw new Error('Thing does not have a name');
console.log(getTheNameOfTheThing(thing)); // Works!
import * as t from 'io-ts';
const ThingWithAName = t.type({
name: t.string
});
type ThingWithAName = t.TypeOf<typeof ThingWithAName>
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
const thing: unknown = { name: 'Name' };
if (!ThingWithAName.is(thing))
throw new Error('Thing does not have a name');
console.log(getTheNameOfTheThing(thing)); // Works!
import * as t from 'io-ts';
const ThingWithAName = t.type({
name: t.string
});
type ThingWithAName = t.TypeOf<typeof ThingWithAName>
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
const thing: unknown = { name: 'Name' };
if (!ThingWithAName.is(thing))
throw new Error('Thing does not have a name');
console.log(getTheNameOfTheThing(thing)); // Works!
import * as t from 'io-ts';
const ThingWithAName = t.type({
name: t.string
});
type ThingWithAName = t.TypeOf<typeof ThingWithAName>
function getTheNameOfTheThing(aThing: ThingWithAName): string {
return aThing.name;
}
const thing: unknown = { name: 'Name' };
if (!ThingWithAName.is(thing))
throw new Error('Thing does not have a name');
console.log(getTheNameOfTheThing(thing)); // Works!
It's easiest to do io-ts if you are the one writing the types.
You can, in theory, write code that will allow io-ts to be a type guard
for a type in code you didn't write or don't control...
...but it's more annoying, and if the upstream code changes that type, you have to rewrite your code as well or risk the same issues that you would get with plain old JavaScript.
type NetID = {
netId: string
hrEmplId?: string // hrEmplId: string | undefined
sesEmplId?: string
}
const NetID = t.intersection([
t.type({
netId: t.string
}),
t.partial({
hrEmplId: t.string,
sesEmplId: t.string
})
]);
import { PathReporter } from 'io-ts/PathReporter';
if (!NetID.is(thing)) {
console.error('Invalid NetID format');
console.error(PathReporter.report(NetID.decode(thing)));
}
Type Narrowing, Predicates, User-Defined Type Guards