Item 40: Prefer Imprecise Types to Inaccurate Types
要点
- Avoid the uncanny valley of type safety: complex but inaccurate types are often worse than simpler, less precise types. If you cannot model a type accurately, do not model it inaccurately! Acknowledge the gaps using
any
orunknown
. - Pay attention to error messages and autocomplete as you make typings increasingly precise. It's not just about correctness: developer experience matters, too.
- As your types grow more complex, your test suite for them should expand.
- 避免类型安全的“怪异谷”:复杂但不准确的类型通常比简单、不精确的类型更糟。如果无法准确建模类型,就不要不准确地建模!可以使用
any
或unknown
来承认类型模型中的空白。 - 在使类型越来越精确时,关注错误信息和自动补全功能。这不仅仅关乎正确性:开发者体验同样重要。
- 随着类型变得更加复杂,针对这些类型的测试用例也应当增加。
正文
ts
interface Point {
type: 'Point'
coordinates: number[]
}
interface LineString {
type: 'LineString'
coordinates: number[][]
}
interface Polygon {
type: 'Polygon'
coordinates: number[][][]
}
type Geometry = Point | LineString | Polygon // Also several others
ts
type GeoPosition = [number, number]
interface Point {
type: 'Point'
coordinates: GeoPosition
}
// Etc.
ts
type Expression1 = any
type Expression2 = number | string | any[]
ts
const okExpressions: Expression2[] = [
10,
'red',
['+', 10, 5],
['rgb', 255, 128, 64],
['case', ['>', 20, 10], 'red', 'blue'],
]
const invalidExpressions: Expression2[] = [
true,
// ~~~ Type 'boolean' is not assignable to type 'Expression2'
['**', 2, 31], // Should be an error: no "**" function
['rgb', 255, 0, 127, 0], // Should be an error: too many values
['case', ['>', 20, 10], 'red', 'blue', 'green'], // (Too many values)
]
ts
type FnName = '+' | '-' | '*' | '/' | '>' | '<' | 'case' | 'rgb'
type CallExpression = [FnName, ...any[]]
type Expression3 = number | string | CallExpression
const okExpressions: Expression3[] = [
10,
'red',
['+', 10, 5],
['rgb', 255, 128, 64],
['case', ['>', 20, 10], 'red', 'blue'],
]
const invalidExpressions: Expression3[] = [
true,
// Error: Type 'boolean' is not assignable to type 'Expression3'
['**', 2, 31],
// ~~ Type '"**"' is not assignable to type 'FnName'
['rgb', 255, 0, 127, 0], // Should be an error: too many values
['case', ['>', 20, 10], 'red', 'blue', 'green'], // (Too many values)
]
ts
type Expression4 = number | string | CallExpression
type CallExpression = MathCall | CaseCall | RGBCall
type MathCall = ['+' | '-' | '/' | '*' | '>' | '<', Expression4, Expression4]
interface CaseCall {
0: 'case'
[n: number]: Expression4
length: 4 | 6 | 8 | 10 | 12 | 14 | 16 // etc.
}
type RGBCall = ['rgb', Expression4, Expression4, Expression4]
ts
const okExpressions: Expression4[] = [
10,
'red',
['+', 10, 5],
['rgb', 255, 128, 64],
['case', ['>', 20, 10], 'red', 'blue'],
]
const invalidExpressions: Expression4[] = [
true,
// ~~~ Type 'boolean' is not assignable to type 'Expression4'
['**', 2, 31],
// ~~~~ Type '"**"' is not assignable to type '"+" | "-" | "/" | ...
['rgb', 255, 0, 127, 0],
// ~ Type 'number' is not assignable to type 'undefined'.
['case', ['>', 20, 10], 'red', 'blue', 'green'],
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Types of property 'length' are incompatible.
// Type '5' is not assignable to type '4 | 6 | 8 | 10 | 12 | 14 | 16'.
]
ts
const moreOkExpressions: Expression4[] = [
['-', 12],
// ~~~~~~ Type '["-", number]' is not assignable to type 'MathCall'.
// Source has 2 element(s) but target requires 3.
['+', 1, 2, 3],
// ~ Type 'number' is not assignable to type 'undefined'.
['*', 2, 3, 4],
// ~ Type 'number' is not assignable to type 'undefined'.
]