Skip to content

Latest commit

 

History

History
282 lines (213 loc) · 12.4 KB

unsoundness.md

File metadata and controls

282 lines (213 loc) · 12.4 KB

Item 48: Avoid Soundness Traps

Things to Remember

  • "Unsoundness" is when a symbol's value at runtime diverges from its static type. It can lead to crashes and other bad behavior without type errors.
  • Be aware of some of the common ways that unsoundness can arise: any types, type assertions (as, is), object and array lookups, and inaccurate type definitions.
  • Avoid mutating function parameters as this can lead to unsoundness. Mark them as read-only if you don't intend to mutate them.
  • Make sure child classes match their parent's method declarations.
  • Be aware of how optional properties can lead to unsound types.

Code Samples

const x = Math.random();
//    ^? const x: number

💻 playground


const xs = [0, 1, 2];
//    ^? const xs: number[]
const x = xs[3];
//    ^? const x: number

💻 playground


console.log(x.toFixed(1));

💻 playground


function logNumber(x: number) {
  console.log(x.toFixed(1));  // x is a string at runtime
  //          ^? (parameter) x: number
}
const num: any = 'forty two';
logNumber(num);  // no error

💻 playground


function logNumber(x: number) {
  console.log(x.toFixed(1));
}
const hour = (new Date()).getHours() || null;
//    ^? const hour: number | null
logNumber(hour);
//        ~~~~ ... Type 'null' is not assignable to type 'number'.
logNumber(hour as number);  // type checks, but might blow up at runtime

💻 playground


if (hour !== null) {
  logNumber(hour);  // ok
  //        ^? const hour: number
}

💻 playground


type IdToName = { [id: string]: string };
const ids: IdToName = {'007': 'James Bond'};
const agent = ids['008'];  // undefined at runtime.
//    ^? const agent: string

💻 playground


const xs = [1, 2, 3];
alert(xs[3].toFixed(1));  // invalid code
//    ~~~~~ Object is possibly 'undefined'.
alert(xs[2].toFixed(1));  // valid code
//    ~~~~~ Object is possibly 'undefined'.

💻 playground


const xs = [1, 2, 3];
for (const x of xs) {
  console.log(x.toFixed(1));  // OK
}
const squares = xs.map(x => x * x);  // also OK

💻 playground


const xs: (number | undefined)[] = [1, 2, 3];
alert(xs[3].toFixed(1));
//    ~~~~~ Object is possibly 'undefined'.

type IdToName = { [id: string]: string | undefined };
const ids: IdToName = {'007': 'James Bond'};
const agent = ids['008'];
//    ^? const agent: string | undefined
alert(agent.toUpperCase());
//    ~~~~~ 'agent' is possibly 'undefined'.

💻 playground


'foo'.replace(/f(.)/, (fullMatch, group1, offset, fullString, namedGroups) => {
  console.log(fullMatch);  // "fo"
  console.log(group1);  // "o"
  console.log(offset);  // 0
  console.log(fullString); // "foo"
  console.log(namedGroups);  // undefined
  return fullMatch;
});

💻 playground


declare function f(): number | string;
const f1: () => number | string | boolean = f;  // OK
const f2: () => number = f;
//    ~~ Type '() => string | number' is not assignable to type '() => number'.
//         Type 'string | number' is not assignable to type 'number'.

💻 playground


declare function f(x: number | string): void;
const f1: (x: number | string | boolean) => void = f;
//    ~~
// Type 'string | number | boolean' is not assignable to type 'string | number'.
const f2: (x: number) => void = f;  // OK

💻 playground


class Parent {
  foo(x: number | string) {}
  bar(x: number) {}
}
class Child extends Parent {
  foo(x: number) {}  // OK
  bar(x: number | string) {}  // OK
}

💻 playground


class FooChild extends Parent  {
  foo(x: number) {
    console.log(x.toFixed());
  }
}
const p: Parent = new FooChild();
p.foo('string');  // No type error, crashes at runtime

💻 playground


function addFoxOrHen(animals: Animal[]) {
  animals.push(Math.random() > 0.5 ? new Fox() : new Hen());
}

const henhouse: Hen[] = [new Hen()];
addFoxOrHen(henhouse); // oh no, a fox in the henhouse!

💻 playground


function addFoxOrHen(animals: readonly Animal[]) {
  animals.push(Math.random() > 0.5 ? new Fox() : new Hen());
  //      ~~~~ Property 'push' does not exist on type 'readonly Animal[]'.
}

💻 playground


function foxOrHen(): Animal {
  return Math.random() > 0.5 ? new Fox() : new Hen();
}

const henhouse: Hen[] = [new Hen(), foxOrHen()];
//                                  ~~~~~~~~~~ error, yay! Chickens are safe.
// Type 'Animal' is missing the following properties from type 'Hen': ...

💻 playground


interface FunFact {
  fact: string;
  author?: string;
}

function processFact(fact: FunFact, processor: (fact: FunFact) => void) {
  if (fact.author) {
    processor(fact);
    console.log(fact.author.blink());  // ok
    //               ^? (property) FunFact.author?: string
  }
}

💻 playground


processFact(
  {fact: 'Peanuts are not actually nuts', author: 'Botanists'},
  f => delete f.author
);
// Type checks, but throws `Cannot read property 'blink' of undefined`.

💻 playground


interface Person {
  name: string;
}
interface PossiblyAgedPerson extends Person {
  age?: number;
}
const p1 = { name: "Serena", age: "42 years" };
const p2: Person = p1;
const p3: PossiblyAgedPerson = p2;
console.log(`${p3.name} is ${p3.age?.toFixed(1)} years old.`);

💻 playground