Skip to content

Item 48: Avoid Soundness Traps

要点

  • "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.
  • “不健全性”(Unsoundness)是指符号的运行时值与其静态类型不一致。这可能导致崩溃和其他不良行为,而没有类型错误的提示。
  • 注意常见的不健全性来源:any 类型、类型断言(asis)、对象和数组查找,以及不准确的类型定义。
  • 避免修改函数参数,因为这可能导致不健全性。如果不打算修改它们,请将参数标记为只读(readonly)。
  • 确保子类的方法声明与父类匹配。
  • 注意可选属性可能导致不健全类型。

正文

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

💻 playground


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

💻 playground


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

💻 playground


ts
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


ts
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


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

💻 playground


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

💻 playground


ts
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


ts
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


ts
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


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


ts
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


ts
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


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

💻 playground


ts
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


ts
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


ts
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


ts
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


ts
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


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

💻 playground


ts
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

Released under the MIT License.