Skip to content

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 or unknown.
  • 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.
  • 避免类型安全的“怪异谷”:复杂但不准确的类型通常比简单、不精确的类型更糟。如果无法准确建模类型,就不要不准确地建模!可以使用 anyunknown 来承认类型模型中的空白。
  • 在使类型越来越精确时,关注错误信息和自动补全功能。这不仅仅关乎正确性:开发者体验同样重要。
  • 随着类型变得更加复杂,针对这些类型的测试用例也应当增加。

正文

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

💻 playground


ts
type GeoPosition = [number, number]
interface Point {
  type: 'Point'
  coordinates: GeoPosition
}
// Etc.

💻 playground


ts
type Expression1 = any
type Expression2 = number | string | any[]

💻 playground


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)
]

💻 playground


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)
]

💻 playground


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]

💻 playground


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'.
]

💻 playground


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'.
]

💻 playground

Released under the MIT License.