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
类型、类型断言(as
、is
)、对象和数组查找,以及不准确的类型定义。 - 避免修改函数参数,因为这可能导致不健全性。如果不打算修改它们,请将参数标记为只读(
readonly
)。 - 确保子类的方法声明与父类匹配。
- 注意可选属性可能导致不健全类型。
正文
ts
const x = Math.random()
// ^? const x: number
ts
const xs = [0, 1, 2]
// ^? const xs: number[]
const x = xs[3]
// ^? const x: number
ts
console.log(x.toFixed(1))
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
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
ts
if (hour !== null) {
logNumber(hour) // ok
// ^? const hour: number
}
ts
type IdToName = { [id: string]: string }
const ids: IdToName = { '007': 'James Bond' }
const agent = ids['008'] // undefined at runtime.
// ^? const agent: string
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'.
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
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'.
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
})
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'.
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
ts
class Parent {
foo(x: number | string) {}
bar(x: number) {}
}
class Child extends Parent {
foo(x: number) {} // OK
bar(x: number | string) {} // OK
}
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
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!
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[]'.
}
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': ...
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
}
}
ts
processFact(
{ fact: 'Peanuts are not actually nuts', author: 'Botanists' },
(f) => delete f.author
)
// Type checks, but throws `Cannot read property 'blink' of undefined`.
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.`)