[译]<<Effective TypeScript>> 技巧26 代码上下文与类型推断

205 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第18天,点击查看活动详情

本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.

技巧26: 代码上下文与类型推断

ts推断变量的类型不仅仅根据 values, 还根据变量所处的上下文. 所以我们需要理解上下文如何应用在类型推断上. 防止意外情况出现.

在js中, 你可以将一个表达式构造成一个常量: (也就是从 Inline 模式变成 Reference 模式)

// Inline form
setLanguage('JavaScript');

// Reference form
let language = 'JavaScript';
setLanguage(language);

在ts中这种重构依旧有效:

function setLanguage(language: string) { /* ... */ }

setLanguage('JavaScript');  // OK

let language = 'JavaScript';
setLanguage(language);  // OK

但是如果你将language类型从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'

let language = 'JavaScript'; 这一句中, ts将language类型推断为string, 导致和函数参数的类型冲突. 有两个好的方法解决这个问题:

  1. 显示类型声明:

    let language: Language = 'JavaScript';
    setLanguage(language);  // OK
    
  2. 将let换成const:

    const language = 'JavaScript';
    setLanguage(language);  // OK
    

    换了之后, language的类型推断为 'JavaScript', 而不是string(更多见技巧 21)

类似的还有其他数据类型也会出类似的问题:

  • tuple :

    / Parameter is a (latitude, longitude) pair.
    function panTo(where: [number, number]) { /* ... */ }
    
    panTo([10, 20]);  // OK
    
    const loc = [10, 20];
    panTo(loc);
    //    ~~~ Argument of type 'number[]' is not assignable to
    //        parameter of type '[number, number]'
    

    这里的const loc = [10, 20];被推断为 number[] 和函数参数不兼容.

    解决办法

    1. 显示类型申明:
      const loc: [number, number] = [10, 20];
      panTo(loc);  // OK
      
    2. const 断言(as const):
      const loc = [10, 20] as const;
      panTo(loc);
         // ~~~ Type 'readonly [10, 20]' is 'readonly'
         //     and cannot be assigned to the mutable type '[number, number]'
      
      这里报错是因为 loc被推断为:readonly [10, 20], 而不是 number[]. 主要是参数 readonly和函数参数产生冲突, 解决办法:
      function panTo(where: readonly [number, number]) { /* ... */ }
      const loc = [10, 20] as const;
      panTo(loc);  // OK
      
      但是const context 也有不足. 就是 在loc定义的时候出错,例如多加了一个元素. 就会产生报错.这个报错的地方是在loc使用的地方, 而不是loc定义的地方:
      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]'
      //          Types of property 'length' are incompatible
      //            Type '3' is not assignable to type '2'
      
  • object 也会有类似的问题:

    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'
    

    解决方法也是一样: 1. 显示类型申明, 2. const断言

  • Callbacks 回调函数:

    正常情况:

    function callWithRandomNumbers(fn: (n1: number, n2: number) => void) {
      fn(Math.random(), Math.random());
    }
    
    callWithRandomNumbers((a, b) => {
      a;  // Type is number
      b;  // Type is number
      console.log(a + b);
    });
    

    从 inline模式 转为 Reference 模式, 发现报错:

    const fn = (a, b) => {
             // ~    Parameter 'a' implicitly has an 'any' type
             //    ~ Parameter 'b' implicitly has an 'any' type
      console.log(a + b);
    }
    callWithRandomNumbers(fn);
    

    解决办法:

    1. 函数参数添加类型申明:
    const fn = (a: number, b: number) => {
      console.log(a + b);
    }
    callWithRandomNumbers(fn);
    
    1. 对整个函数添加类型声明: 见技巧 12