typescript学习总结(二)

430 阅读22分钟

4字面量类型、类型推断、类型拓宽和类型缩小

4.1引入

在很多情况下,TypeScript 会根据上下文环境自动推断出变量的类型,无须我们再写明类型注解。

{
  let str: string = 'this is string';
  let num: number = 1;
  let bool: boolean = true;
}
{
  const str: string = 'this is string';
  const num: number = 1;
  const bool: boolean = true;
}

上面的示例可以简化为如下所示内容:

{
  let str = 'this is string'; // 等价
  let num = 1; // 等价
  let bool = true; // 等价
}
{
  const str = 'this is string'; // 不等价
  const num = 1; // 不等价
  const bool = true; // 不等价
}

注意:这里说的仅仅是“简化”,而不是说两个示例完全等价,接下来我们会进一步介绍。

我们把 TypeScript 这种基于赋值表达式推断类型的能力称之为“类型推断”

4.2类型推断

在 TypeScript 中,类型标注声明是在变量之后(即类型后置).

使用类型标注后置的好处是编译器可以通过代码所在的上下文推导其对应的类型,无须再声明变量类型,具体示例如下:

{
  let x1 = 42; // 推断出 x1 的类型是 number
  let x2: number = x1; // ok
}

在 TypeScript 中,具有初始化值的变量、有默认值的函数参数、函数返回的类型都可以根据上下文推断出来。比如我们能根据 return 语句推断函数返回的类型,如下代码所示:

{
  /** 根据参数的类型,推断出返回值的类型也是 number */
  function add1(a: number, b: number) {
    return a + b;
  }
  const x1= add1(1, 1); // 推断出 x1 的类型也是 number

  /** 推断参数 b 的类型是数字或者 undefined,返回值的类型也是数字 */
  function add2(a: number, b = 1) {
    return a + b;
  }
  const x2 = add2(1);
  const x3 = add2(1, '1'); // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number | undefined
}

4.3上下文推断

通过类型推断的例子,我们发现变量的类型可以通过被赋值的值进行推断。除此之外,在某些特定的情况下,我们也可以通过变量所在的上下文环境推断变量的类型,具体示例如下:

{
  type Adder = (a: number, b: number) => number;
  const add: Adder = (a, b) => {
    return a + b;
  }
  const x1 = add(1, 1); // 推断出 x1 类型是 number
  const x2 = add(1, '1');  // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number
}

这里我们定义了一个实现加法功能的函数类型 Adder(定义的 Adder 类型使用了 type 类型别名,这点会在 07 讲专门介绍),声明了add变量的类型为 Adder 并赋值一个匿名箭头函数,箭头函数参数 a 和 b 的类型和返回类型都没有显式声明。

TypeScript 通过add的类型 Adder 反向(通过变量类型推断出值的相关类型)推断出箭头函数参数及返回值的类型,也就是说函数参数 a、b,以及返回类型在这个变量的声明上下文中被确定了(更多关于函数类型相关的知识点会在 05 讲中详细介绍)。

正是得益于 TypeScript 这种类型推导机制和能力,使得我们无须显式声明,即可直接通过上下文环境推断出变量的类型,也就是说此时类型可缺省。

下面回头看最前面的示例(如下所示),我们发现这些缺省类型注解的变量还可以通过类型推断出类型。

{
  let str = 'this is string'; // str: string
  let num = 1; // num: number
  let bool = true; // bool: boolean
}
{
  const str = 'this is string'; // str: 'this is string'
  const num = 1; // num: 1
  const bool = true; // bool: true
}

如上述代码中注释说明,通过 let 和 const 定义的赋予了相同值的变量,其推断出来的类型不一样。比如同样是 'this is string'(这里表示一个字符串值),通过 let 定义的变量类型是 string,而通过 const 定义的变量类型是 'this is string'(这里表示一个字符串字面量类型)。这里我们可以通过 VS Code hover 示例中的变量查看类型,验证一下这个结论。

4.4字面量类型

在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。

目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型.

{
  let specifiedStr: 'this is string' = 'this is string';
  let specifiedNum: 1 = 1;
  let specifiedBoolean: true = true;
}

字面量类型是集合类型的子类型,它是集合类型的一种更具体的表达。比如 'this is string' (这里表示一个字符串字面量类型)类型是 string 类型(确切地说是 string 类型的子类型),而 string 类型不一定是 'this is string'(这里表示一个字符串字面量类型)类型.

{
  let specifiedStr: 'this is string' = 'this is string';
  let str: string = 'any string';
  specifiedStr = str; // ts(2322) 类型 '"string"' 不能赋值给类型 'this is string'
  str = specifiedStr; // ok 
}

4.5字符串字面量类型

一般来说,我们可以使用一个字符串字面量类型作为变量的类型

let hello: 'hello' = 'hello';
hello = 'hi'; // ts(2322) Type '"hi"' is not assignable to type '"hello"'

实际上,定义单个的字面量类型并没有太大的用处,它真正的应用场景是可以把多个字面量类型组合成一个联合类型(详见 08 讲),用来描述拥有明确成员的实用的集合。

如下代码所示,我们使用字面量联合类型描述了一个明确、可 'up' 可 'down' 的集合,这样就能清楚地知道需要的数据结构了。

type Direction = 'up' | 'down';
function move(dir: Direction) {
  // ...
}
move('up'); // ok
move('right'); // ts(2345) Argument of type '"right"' is not assignable to parameter of type 'Direction'

通过使用字面量类型组合的联合类型,我们可以限制函数的参数为指定的字面量类型集合,然后编译器会检查参数是否是指定的字面量类型集合里的成员。

因此,相较于使用 string 类型,使用字面量类型(组合的联合类型)可以将函数的参数限定为更具体的类型。这不仅提升了程序的可读性,还保证了函数的参数类型,可谓一举两得。

4.6数字字面量类型及布尔字面量类型

我们可以使用字面量组合的联合类型将函数的参数限定为更具体的类型,比如声明如下所示的一个类型 Config:

interface Config {
    size: 'small' | 'big';
    isEnable:  true | false;
    margin: 0 | 2 | 4;
}

在上述代码中,我们限定了 size 属性为字符串字面量类型 'small' | 'big',isEnable 属性为布尔字面量类型 true | false(布尔字面量只包含 true 和 false,true | false 的组合跟直接使用 boolean 没有区别),margin 属性为数字字面量类型 0 | 2 | 4。

我们再来看看通过 let 和 const 定义的变量的值相同,而变量类型不一致的具体原因。

{
  const str = 'this is string'; // str: 'this is string'
  const num = 1; // num: 1
  const bool = true; // bool: true
}

在上述代码中,我们将 const 定义为一个不可变更的常量,在缺省类型注解的情况下,TypeScript 推断出它的类型直接由赋值字面量的类型决定,这也是一种比较合理的设计。

接下来我们看看如下所示的 let 示例,此时理解起来可能会稍微难一些。

{
  let str = 'this is string'; // str: string
  let num = 1; // num: number
  let bool = true; // bool: boolean
}

在上述代码中,缺省显式类型注解的可变更的变量的类型转换为了赋值字面量类型的父类型,比如 str 的类型是 'this is string' 类型(这里表示一个字符串字面量类型)的父类型 string,num 的类型是 1 类型的父类型 number。

这种设计符合编程预期,意味着我们可以分别赋予 str 和 num 任意值(只要类型是 string 和 number 的子集的变量):

 str = 'any string';
  num = 2;
  bool = false;

我们将 TypeScript 的字面量子类型转换为父类型的这种设计称之为 "literal widening",也就是字面量类型的拓宽,比如上面示例中提到的字符串字面量类型转换成 string 类型,下面我们着重介绍一下。

4.7Literal Widening

所有通过 let 或 var 定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。

{
  let str = 'this is string'; // 类型是 string
  let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string;
  const specifiedStr = 'this is string'; // 类型是 'this is string'
  let str2 = specifiedStr; // 类型是 'string'
  let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;
}

因为第 2~3 行满足了 let、形参且未显式声明类型注解的条件,所以变量、形参的类型拓宽为 string(形参类型确切地讲是 string | undefined)。

因为第 4 行的常量不可变更,类型没有拓宽,所以 specifiedStr 的类型是 'this is string' 字面量类型。

第 5~6 行,因为赋予的值 specifiedStr 的类型是字面量类型,且没有显式类型注解,所以变量、形参的类型也被拓宽了。其实,这样的设计符合实际编程诉求。我们设想一下,如果 str2 的类型被推断为 'this is string',它将不可变更,因为赋予任何其他的字符串类型的值都会提示类型错误。

基于字面量类型拓宽的条件,我们可以通过如下所示代码添加显示类型注解控制类型拓宽行为

{
  const specifiedStr: 'this is string' = 'this is string'; // 类型是 '"this is string"'
  let str2 = specifiedStr; // 即便使用 let 定义,类型是 'this is string'
}

4.8Type Widening

通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any:

{
  let x = null; // 类型拓宽成 any
  let y = undefined; // 类型拓宽成 any
  /** -----分界线------- */
  const z = null; // 类型是 null
  /** -----分界线------- */
  let anyFun = (param = null) => param; // 形参类型是 null
  let z2 = z; // 类型是 null
  let x2 = x; // 类型是 null
  let y2 = y; // 类型是 undefined
}

在现代 TypeScript 中,以上示例的第 2~3 行的类型拓宽更符合实际编程习惯,我们可以赋予任何其他类型的值给具有 null 或 undefined 初始值的变量 x 和 y。

示例第 7~10 行的类型推断行为因为开启了 strictNullChecks=true(说明:本课程所有示例都基于严格模式编写),此时我们可以从类型安全的角度试着思考一下:这几行代码中出现的变量、形参的类型为什么是 null 或 undefined,而不是 any?因为前者可以让我们更谨慎对待这些变量、形参,而后者不能。

4.9Type Narrowing

在 TypeScript 中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是 "Type Narrowing"。

{
  let func = (anything: any) => {
    if (typeof anything === 'string') {
      return anything; // 类型是 string 
    } else if (typeof anything === 'number') {
      return anything; // 类型是 number
    }
    return null;
  };
}

我们可以使用类型守卫将联合类型(详见 08 讲 内容)缩小到明确的子类型,具体示例如下:

{
  let func = (anything: string | number) => {
    if (typeof anything === 'string') {
      return anything; // 类型是 string 
    } else {
      return anything; // 类型是 number
    }
  };
}

当然,我们也可以通过字面量类型等值判断(===)或其他控制流语句(包括但不限于 if、三目运算符、switch 分支)将联合类型收敛为更具体的类型,如下代码所示:

{
  type Goods = 'pen' | 'pencil' |'ruler';
  const getPenCost = (item: 'pen') => 2;
  const getPencilCost = (item: 'pencil') => 4;
  const getRulerCost = (item: 'ruler') => 6;
  const getCost = (item: Goods) =>  {
    if (item === 'pen') {
      return getPenCost(item); // item => 'pen'
    } else if (item === 'pencil') {
      return getPencilCost(item); // item => 'pencil'
    } else {
      return getRulerCost(item); // item => 'ruler'
    }
  }
}

在上述 getCost 函数中,接受的参数类型是字面量类型的联合类型,函数内包含了 if 语句的 3 个流程分支,其中每个流程分支调用的函数的参数都是具体独立的字面量类型

那为什么类型由多个字面量组成的变量 item 可以传值给仅接收单一特定字面量类型的函数 getPenCost、getPencilCost、getRulerCost 呢?这是因为在每个流程分支中,编译器知道流程分支中的 item 类型是什么。比如 item === 'pencil' 的分支,item 的类型就被收缩为“pencil”。

事实上,如果我们将上面的示例去掉中间的流程分支,编译器也可以推断出收敛后的类型,如下代码所示:

const getCost = (item: Goods) =>  {
    if (item === 'pen') {
      item; // item => 'pen'
    } else {
      item; // => 'pencil' | 'ruler'
    }
  }

5函数类型:返回值类型和参数类型

我们可以使用函数抽离可复用的逻辑、抽象模型、封装过程。在 TypeScript 中,虽然有类、命名空间、模块,但是函数同样是最基本、最重要的元素之一。

在 TypeScript 里,我们可以通过 function 字面量和箭头函数的形式定义函数

function add() {}
const add = () => {}

我们还可以显式指定函数参数和返回值的类型

const add = (a: number, b: number): number => {
     return a + b;
}

5.1返回值类型

在 JavaScript 中,我们知道一个函数可以没有显式 return,此时函数的返回值应该是 undefined:

function fn() {
  // TODO
}
console.log(fn()); // => undefined

需要注意的是,在 TypeScript 中,如果我们显式声明函数的返回值类型为 undfined,将会得到如下所示的错误提醒。

function fn(): undefined { // ts(2355) A function whose declared type is neither 'void' nor 'any' must return a value
  // TODO
}

正确的做法是使用 void 类型来表示函数没有返回值的类型(这是“废柴” void 类型唯一有用的场景)

function fn1(): void {
}
fn1().doSomething(); // ts(2339) Property 'doSomething' does not exist on type 'void'.

我们可以使用类似定义箭头函数的语法来表示函数类型的参数和返回值类型,此时=> 类型仅仅用来定义一个函数类型而不用实现这个函数。

需要注意的是,这里的=>与 ES6 中箭头函数的=>有所不同。TypeScript 函数类型中的=>用来表示函数的定义,其左侧是函数的参数类型,右侧是函数的返回值类型;而 ES6 中的=>是函数的实现。

type Adder = (a: number, b: number) => number; // TypeScript 函数类型定义
const add: Adder = (a, b) => a + b; // ES6 箭头函数

这里请注意:右侧的箭头函数并没有显式声明类型注解,不过可以根据 04 讲中提到的上下文类型进行推断

对象(即接口类型)中,除了使用这种声明语法,我们还可以使用类似对象属性的简写语法来声明函数类型的属性,如下代码所示:

interface Entity {
    add: (a: number, b: number) => number;
    del(a: number, b: number): number;
}
const entity: Entity = {
    add: (a, b) => a + b,
    del(a, b) {
      return a - b;
    },
};

5.2可缺省和可推断的返回值类型

函数返回值的类型可以在 TypeScript 中被推断出来,即可缺省

函数内是一个相对独立的上下文环境,我们可以根据入参对值加工计算,并返回新的值。从类型层面看,我们也可以通过类型推断(回想一下 04 讲中的类型推断、上下文类型推断)加工计算入参的类型,并返回新的类型,示例如下:

function computeTypes(one: string, two: number) {
  const nums = [two];
  const strs = [one]
  return {
    nums,
    strs
  } // 返回 { nums: number[]; strs: string[] } 的类型 
}

请记住:这是一个很重要也很有意思的特性,函数返回值的类型推断结合泛型(我们会在 10 讲中详细介绍)可以实现特别复杂的类型计算(本质是复杂的类型推断,这里称之为计算是为了表明其复杂性),比如 Redux Model 中 State、Reducer、Effect 类型的关联。

一般情况下,TypeScript 中的函数返回值类型是可以缺省和推断出来的,但是有些特例需要我们显式声明返回值类型,比如 Generator 函数的返回值。

5.3Generator 函数的返回值

Generator 函数返回的是一个 Iterator 迭代器对象,我们可以使用 Generator 的同名接口泛型或者 Iterator 的同名接口泛型表示返回值的类型(Generator 类型继承了 Iterator 类型),示例如下:

type AnyType = boolean;
type AnyReturnType = string;
type AnyNextType = number;
function *gen(): Generator<AnyType, AnyReturnType, AnyNextType> {
  const nextValue = yield true; // nextValue 类型是 number,yield 后必须是 boolean 类型
  return `${nextValue}`; // 必须返回 string 类型
}

5.4可选参数和默认参数

function log(x?: string) {
  return x;
}
log(); // => undefined
log('hello world'); // => hello world

我们在类型标注的:前添加?表示 log 函数的参数 x 就是可缺省的

这并不是意味着可缺省和类型是 undefined 等价的。

function log(x?: string) {
  console.log(x);
}
function log1(x: string | undefined) {
  console.log(x);
}
log();
log(undefined);
log1(); // ts(2554) Expected 1 arguments, but got 0
log1(undefined);

这里的 ?: 表示参数可以缺省、可以不传,也就是说调用函数时,我们可以不显式传入参数。但是,如果我们声明了参数类型为 xxx | undefined(这里使用了联合类型 |),就表示函数参数是不可缺省且类型必须是 xxx 或者 undfined。

在 ES6 中支持函数默认参数的功能,而 TypeScript 会根据函数的默认参数的类型来推断函数参数的类型

function log(x = 'hello') {
    console.log(x);
}
log(); // => 'hello'
log('hi'); // => 'hi'
log(1); // ts(2345) Argument of type '1' is not assignable to parameter of type 'string | undefined'

根据函数的默认参数 'hello' ,TypeScript 推断出了 x 的类型为 string | undefined

当然,对于默认参数,TypeScript 也可以显式声明参数的类型(一般默认参数的类型是参数类型的子集时,我们才需要这么做)。不过,此时的默认参数只起到参数默认值的作用

function log1(x: string = 'hello') {
    console.log(x);
}
// ts(2322) Type 'string' is not assignable to type 'number'
function log2(x: number = 'hello') {
    console.log(x);
}
log2();
log2(1);
log2('1'); // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number | undefined'

上例函数 log2 中,我们显式声明了函数参数 x 的类型为 number,表示函数参数 x 的类型可以不传或者是 number 类型。

这里请注意:函数的默认参数类型必须是参数类型的子类型

function log3(x: number | string = 'hello') {
    console.log(x);
}

函数 log3 的函数参数 x 的类型为可选的联合类型 number | string,但是因为默认参数字符串类型是联合类型 number | string 的子类型,所以 TypeScript 也会检查通过。

5.5剩余参数

在 ES6 中,JavaScript 支持函数参数的剩余参数,它可以把多个参数收集到一个变量中。同样,在TypeScript 中也支持这样的参数类型定义

function sum(...nums: number[]) {
    return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2); // => 3
sum(1, 2, 3); // => 6
sum(1, '2'); // ts(2345) Argument of type 'string' is not assignable to parameter of type 'number'

如果我们将函数参数 nums 聚合的类型定义为 (number | string)[]

function sum(...nums: (number | string)[]): number {
    return nums.reduce<number>((a, b) => a + Number(b), 0);
}
sum(1, '2', 3); // 6
console.log(sum('1', '2', 3));//6

那么,函数的每一个参数的类型就是联合类型 number | string,因此 sum(1, '2', 3) 的类型检查也就通过了。

5.6this

众所周知,在 JavaScript 中,函数 this 的指向一直是一个令人头痛的问题。因为 this 的值需要等到函数被调用时才能被确定,更别说通过一些方法还可以改变 this 的指向。也就是说 this 的类型不固定,它取决于执行时的上下文。

但是,使用了 TypeScript 后,我们就不用担心这个问题了。通过指定 this 的类型(严格模式下,必须显式指定 this 的类型),当我们错误使用了 this,TypeScript 就会提示我们,如下代码所示:

function say() {
    console.log(this.name); // ts(2683) 'this' implicitly has type 'any' because it does not have a type annotation
}
say();

在上述代码中,如果我们直接调用 say 函数,this 应该指向全局 window 或 global(Node 中)。但是,在 strict 模式下的 TypeScript 中,它会提示 this 的类型是 any,此时就需要我们手动显式指定类型了。

在 TypeScript 中,我们只需要在函数的第一个参数中声明 this 指代的对象(即函数被调用的方式)即可,比如最简单的作为对象的方法的 this 指向

function say(this: Window, name: string) {
    console.log(this.name);
}
window.say = say;
window.say('hi');
const obj = {
    say
};
obj.say('hi'); // ts(2684) The 'this' context of type '{ say: (this: Window, name: string) => void; }' is not assignable to method's 'this' of type 'Window'.

调用obj.say()后,此时 TypeScript 检测到 this 的指向不是 window

say('captain'); // ts(2684) The 'this' context of type 'void' is not assignable to method's 'this' of type 'Window'

需要注意的是,如果我们直接调用 say(),this 实际上应该指向全局变量 window,但是因为 TypeScript 无法确定 say 函数被谁调用,所以将 this 的指向默认为 void,也就提示了一个 ts(2684) 错误。

此时,我们可以通过调用 window.say() 来避免这个错误,这也是一个安全的设计。因为在 JavaScript 的严格模式下,全局作用域函数中 this 的指向是 undefined。

同样,定义对象的函数属性时,只要实际调用中 this 的指向与指定的 this 指向不同,TypeScript 就能发现 this 指向的错误,示例代码如下:

interface Person {
    name: string;
    say(this: Person): void;
}
const person: Person = {
    name: 'captain',
    say() {
        console.log(this.name);
    },
};
const fn = person.say;
fn(); // ts(2684) The 'this' context of type 'void' is not assignable to method's 'this' of type 'Person'

注意:显式注解函数中的 this 类型,它表面上占据了第一个形参的位置,但并不意味着函数真的多了一个参数,因为 TypeScript 转译为 JavaScript 后,“伪形参” this 会被抹掉,这算是 TypeScript 为数不多的特有语法。

当然,初次接触这个特性时让人费解,这就需要我们把它铭记于心。前边的 say 函数转译为 JavaScript 后,this 就会被抹掉

function say(name) {
    console.log(this.name);
}

同样,我们也可以显式限定类(class 类的介绍详见 06 讲)函数属性中的 this 类型,TypeScript 也能检查出错误的使用方式

class Component {
  onClick(this: Component) {}
}
const component = new Component();
interface UI {
  addClickListener(onClick: (this: void) => void): void;
}
const ui: UI = {
  addClickListener() {}
};
ui.addClickListener(new Component().onClick); // ts(2345)

此外,在链式调用风格的库中,使用 this 也可以很方便地表达出其类型

class Container {
  private val: number;
  constructor(val: number) {
    this.val = val;
  }
  map(cb: (x: number) => number): this {
    this.val = cb(this.val);
    return this;
  }
  log(): this {
    console.log(this.val);
    return this;
  }
}
const instance = new Container(1)
  .map((x) => x + 1)
  .log() // => 2
  .map((x) => x * 3)
  .log(); // => 6  

因为 Container 类中 map、log 等函数属性(方法)未显式指定 this 类型,默认类型是 Container,所以以上方法在被调用时返回的类型也是 Container,this 指向一直是类的实例,它可以一直无限地被链式调用。

5.7函数重载

JavaScript 是一门动态语言,针对同一个函数,它可以有多种不同类型的参数与返回值,这就是函数的多态

function convert(x: string | number | null): string | number | -1 {
    if (typeof x === 'string') {
        return Number(x);
    }
    if (typeof x === 'number') {
        return String(x);
    }
    return -1;
}
const x1 = convert('1'); // => string | number
const x2 = convert(1); // => string | number
const x3 = convert(null); // => string | number

那么,有没有一种办法可以更精确地描述参数与返回值类型约束关系的函数类型呢?有,这就是函数重载(Function Overload),如下示例中 1~3 行定义了三种各不相同的函数类型列表,并描述了不同的参数类型对应不同的返回值类型,而从第 4 行开始才是函数的实现。

function convert(x: string): number;
function convert(x: number): string;
function convert(x: null): -1;
function convert(x: string | number | null): any {
    if (typeof x === 'string') {
        return Number(x);
    }
    if (typeof x === 'number') {
        return String(x);
    }
    return -1;
}
const x1 = convert('1'); // => number
const x2 = convert(1); // => string
const x3 = convert(null); // -1

注意: 函数重载列表的各个成员(即示例中的 1 ~ 3 行)必须是函数实现(即示例中的第 4 行)的子集,例如 “function convert(x: string): number”是“function convert(x: string | number | null): any”的子集。

在 convert 函数被调用时,TypeScript 会从上到下查找函数重载列表中与入参类型匹配的类型,并优先使用第一个匹配的重载定义。因此,我们需要把最精确的函数重载放到前面

interface P1 {
    name: string;
}
interface P2 extends P1 {
    age: number;
}
function convert(x: P1): number;
function convert(x: P2): string;
function convert(x: P1 | P2): any {}
const x1 = convert({ name: "" } as P1); // => number
const x2 = convert({ name: "", age: 18 } as P2); // number

因为 P2 继承自 P1,所以类型为 P2 的参数会和类型为 P1 的参数一样匹配到第一个函数重载,此时 x1、x2 的返回值都是 number。

function convert(x: P2): string;
function convert(x: P1): number;
function convert(x: P1 | P2): any { }
const x1 = convert({ name: '' } as P1); // => number
const x2 = convert({ name: '', age: 18 } as P2); // => string

而我们只需要将函数重载列表的顺序调换一下,类型为 P2 和 P1 的参数就可以分别匹配到正确的函数重载了

5.8类型谓词(is)

function isString(s): s is string { // 类型谓词
  return typeof s === 'string';
}
function isNumber(n: number) {
  return typeof n === 'number';
}
function operator(x: unknown) {
  if(isString(x)) { // ok x 类型缩小为 string
  }
  if (isNumber(x)) { // ts(2345) unknown 不能赋值给 number
  }
}

在上述代码中,在添加返回值类型的地方,我们通过“参数名 + is + 类型”的格式明确表明了参数的类型,进而引起类型缩小,所以类型谓词函数的一个重要的应用场景是实现自定义类型守卫。

类型谓词只能用来定义自定义类型守卫,实际上是告诉引擎,当守卫返回 true 的时候,将被守卫的类型缩小到 is 指定的更明确的类型。