第 24 条: 理解上下文在类型推断中的作用 
要点 
- 了解上下文如何在类型推断中被使用。
 - 如果提取变量时引入了类型错误,可以考虑添加类型注解。
 - 如果变量确实是常量,使用 
const断言(as const)。但要注意,这可能会导致错误在使用时而非定义时出现。 - 在可行的情况下,优先内联值,以减少对类型注解的需求。
 
正文 
TypeScript 不只是根据值来推断类型,它还会考虑值所处的上下文环境。这种做法大多数时候都很智能,但有时也会带来一些意想不到的问题。理解 TypeScript 如何使用上下文来进行类型推断,可以帮助你在遇到这些“惊喜”时更好地应对。
在 JavaScript 中,只要不改变执行顺序,你完全可以把某个表达式提取出来赋值给常量,而不会影响代码的行为。换句话说,下面这两段代码在逻辑上是一样的:
// Inline form
setLanguage('JavaScript')
// Reference form
let language = 'JavaScript'
setLanguage(language)在 TypeScript 中,这样的重构依然是可行的:
function setLanguage(language: string) {
  /* ... */
}
setLanguage('JavaScript') // OK
let language = 'JavaScript'
setLanguage(language) // OK但如果你认真采纳了第 35 条建议,用更精确的字符串字面量联合类型来替代普通的 string 类型,比如这样:
type Language = 'JavaScript' | 'TypeScript' | 'Python'
function setLanguage(language: Language) {
  /* ... */
}
setLanguage('JavaScript') // OK
let language = 'JavaScript'
setLanguage(language)
//          ~~~~~~~~ Argument of type 'string' is not assignable
//                   to parameter of type 'Language'出了什么问题?在内联形式中,TypeScript 从函数声明中就知道参数应该是 Language 类型。字符串字面量 'JavaScript' 可以赋值给这个类型,所以没问题。但当你把它抽取成变量时,TypeScript 必须在赋值时推断变量的类型。它会用常规算法(见第 20 条)推断出 string 类型,而 string 不能赋值给 Language 类型,因此报错。
NOTE
有些语言可以根据变量的最终用途来推断类型,但这也可能让人困惑。TypeScript 的创造者 Anders Hejlsberg 把这种现象称为“远距离的幽灵行动”。大多数情况下,TypeScript 会在变量第一次出现时确定它的类型。想了解此规则的一个显著例外,请参见第 25 条。
解决这个问题有两种好方法。其中一种是用类型注解来限制 language 变量的可能取值:
let language: Language = 'JavaScript'
setLanguage(language) // OK这还有一个好处:如果 language 写错了,比如写成 'Typescript'(应该是大写的“S”),它会报错提醒你。
另一种解决方案是把变量声明为常量:
const language = 'JavaScript'
//    ^? const language: "JavaScript"
setLanguage(language) // OK用 const 表示这个变量不会变,所以 TypeScript 能推断出更精确的类型——字符串字面量类型 "JavaScript"。这个类型是可以赋值给 Language 的,所以代码能通过类型检查。当然,如果你需要重新赋值,就必须用类型注解了。
这里的根本问题是,我们把值和它被使用的上下文分开了。有时候这样没问题,但很多时候会出错。接下来会讲几个因失去上下文导致错误的例子,并教你怎么修复。
元组类型(Tuple Types) 
除了字符串字面量类型,元组类型也会有类似问题。比如你在写一个地图的程序,可以用代码控制地图平移:
// Parameter is a (latitude, longitude) pair.
function panTo(where: [number, number]) {
  /* ... */
}
panTo([10, 20]) // OK
const loc = [10, 20]
//    ^? const loc: number[]
panTo(loc)
//    ~~~ Argument of type 'number[]' is not assignable to
//        parameter of type '[number, number]'和之前一样,你把值和上下文分开了。[10, 20] 直接用时符合元组类型 [number, number]。但是 loc 被推断成了 number[],也就是长度不确定的数字数组,这就不能赋值给严格长度的元组。
怎么解决呢?你已经用了 const,不能再用它了,但可以加类型注解告诉 TypeScript 你具体想要什么:
const loc: [number, number] = [10, 20]
panTo(loc) // OK另一种方法是用“const 上下文”告诉 TypeScript 你想让这个值是深度不可变的,而不仅仅是浅层 const:
const loc = [10, 20] as const
//    ^? const loc: readonly [10, 20]
panTo(loc)
//    ~~~ The type 'readonly [10, 20]' is 'readonly'
//        and cannot be assigned to the mutable type '[number, number]'这时 loc 的类型是 readonly [10, 20],比之前更精确了。但这个类型是只读的,而 panTo 函数的参数是可变的,所以不能赋值过去。
最好的办法是给 panTo 函数加个 readonly 注解:
function panTo(where: readonly [number, number]) {
  /* ... */
}
const loc = [10, 20] as const
panTo(loc) // OK如果你没法改 panTo 的定义,那就只能用类型注解了。(第 14 条讲了 readonly 和类型安全的更多内容。)
使用 const 上下文能解决推断丢失上下文的问题,但有个缺点:如果你定义错了(比如给元组多加了一个元素),错误不会在定义处报,而是在调用处报,可能很难发现:
const loc = [10, 20, 30] as const // error is really here.
panTo(loc)
//    ~~~ Argument of type 'readonly [10, 20, 30]' is not assignable to
//        parameter of type 'readonly [number, number]'
//          Source has 3 element(s) but target allows only 2.因此,最好还是用内联形式或者加类型声明。
对象类型 
把值和上下文分开也会在对象里出错,特别是对象里有字符串字面量或元组时。例如:
type Language = 'JavaScript' | 'TypeScript' | 'Python'
interface GovernedLanguage {
  language: Language
  organization: string
}
function complain(language: GovernedLanguage) {
  /* ... */
}
complain({ language: 'TypeScript', organization: 'Microsoft' }) // OK
const ts = {
  language: 'TypeScript',
  organization: 'Microsoft',
}
complain(ts)
//       ~~ Argument of type '{ language: string; organization: string; }'
//            is not assignable to parameter of type 'GovernedLanguage'
//          Types of property 'language' are incompatible
//            Type 'string' is not assignable to type 'Language'这里 ts.language 被推断成了 string 类型,而不是具体的 Language 类型。解决办法同样是加类型注解:
const ts: GovernedLanguage = {
  language: 'TypeScript',
  organization: 'Microsoft',
}或者用 as const 断言,或者用 satisfies 操作符(见第 20 条)。
回调函数 
当你把回调函数作为参数传给别的函数时,TypeScript 会用上下文推断回调的参数类型:
function callWithRandomNumbers(fn: (n1: number, n2: number) => void) {
  fn(Math.random(), Math.random())
}
callWithRandomNumbers((a, b) => {
  //                   ^? (parameter) a: number
  console.log(a + b)
  //              ^? (parameter) b: number
})但如果你把回调单独提出来,失去上下文,TypeScript 就推断不出来,会报 noImplicitAny 错误:
const fn = (a, b) => {
  //        ~    Parameter 'a' implicitly has an 'any' type
  //           ~ Parameter 'b' implicitly has an 'any' type
  console.log(a + b)
}
callWithRandomNumbers(fn)解决办法是给参数加类型注解:
const fn = (a: number, b: number) => {
  console.log(a + b)
}
callWithRandomNumbers(fn)或者给整个函数表达式加类型声明(见第 12 条)。如果函数只用一次,推荐用内联写法,省去额外注解。
关键点总结 
- 了解上下文如何在类型推断中被使用。
 - 如果提取变量时引入了类型错误,可以考虑添加类型注解。
 - 如果变量确实是常量,使用 
const断言(as const)。但要注意,这可能会导致错误在使用时而非定义时出现。 - 在可行的情况下,优先内联值,以减少对类型注解的需求。