TypeScript 重点知识回顾(一)

231 阅读5分钟

结构类型系统 VS 标称类型系统 VS 鸭子类型

我们知道 JavaScript 是动态脚本语言,其中鸭子类型( Duck Type ) 应用广泛,注意一个误区,鸭子类型不是一种类型,而是一种动态类型的风格,不能同 "字符串类型" , "浮点类型" 等比较。

鸭子类型

简单说,鸭子类型就是,一个函数不关心传入参数的类型,只关心这个参数有没有函数想要的方法和属性,如果有就能运行,如果没有就不能运行。就好比我们看到一只鸟,只要它能发出鸭子叫,像鸭子一样走路,那我就认为他是一只鸭子,只看结果。

我们用一个 JavaScript 中常见的一个例子来理解一下

const arrLike = { 
    '0': 1, 
    '1': 2, 
    '2': 3, 
    length: 3 
}

// [].slice === Array.prototype.slice ,下同
[].slice.call(arrLike) // [1, 2, 3]

[].map.call(arrLike, item => item + 1) // [2, 3, 4]

[].filter.call(arrLike, item => item !== 2) // [1, 3]

[].reduce.call(arrLike, (prev, curr) => prev + curr, 0) // 6

[].map.call('123', Number) // [1, 2, 3]

例解:我们知道 JavaScript 中数组上有 map,reduce 等方法,而对象上没有,我们上例上定义了一个对象类型,但是在其中定义了数组类型的特征,就好比我们给鸟定义了鸭子叫和鸭子走路一样,这时候 JavaScript 就会把这个对象当做数组来处理。这种对象也叫作类数组对象

结构类型与标称类型

结构类型 是 TypeScript 考虑到 JavaScript 的灵活特性,基于 C# 的标称类型中诞生的,两者有诸多相似,我们通过一个例子解释两者的不同。

标称类型( C# )

public class Foo  
{
    public string Name { get; set; }
    public int Id { get; set;}
}

public class Bar  
{
    public string Name { get; set; }
    public int Id { get; set; }
}

Foo foo = new Foo(); // Okay.
Bar bar = new Foo(); // Error!!!

结构类型( TypeScript )

class Foo {
  method(input: string): number { ... }
}

class Bar {
  method(input: string): number { ... }
}

const foo: Foo = new Foo(); // Okay.
const bar: Bar = new Foo(); // Okay.

小结:通过对比,我们很清晰的找到差别,标称类型比较的是 类型本身 , 而结构类型比较的是 类型定义的形状,所以标称类型即使形状相同也不能赋值,而结构类型,只要内容相同可以满足原有类型中定义的,即可赋值。

泛型中的 K,T , V , E

T ( Type ) : 表示类型。

K ( Key ) : 表示对象中键的类型。

V ( Value ) : 表示对象中值的类型。

E ( Element ) : 表示元素类型。

小结:这些并不是固定定义,你可以使用任意的大写字母缩写来代表你想要指定的类型,但是编程界一直有一句老话,约定大于配置,所以一些通用的缩写大家需要记住。

单值类型与双值类型

在 TypeScript 中,每一个数字都可以作为一个类型,也叫作单值类型,即只能赋值这个数字给这个类型。

Boolearn 类型则拥有两个选择 true 和 false 所以也被称为双值类型。

let a : 1 = 1
let b : 2 = 3// Errors in code  Type '3' is not assignable to type '2'.

联合类型与交叉类型

联合类型 增加选择范围,即一直要符合该联合类型中的一种,即可通过。

我们看个案例理解

const unite = (name: string | undefined) => {
  /* ... */
};
unite('coolFish')
unite(undefined)
unite(123)//Errors in code 
//Argument of type 'number' is not assignable to parameter of type 'string'.
//'name' is declared but its value is never read.

交叉类型 是将多个类型合并为一个类型。通过 & 运算符可以将现有的多种类型叠加到一起称为一种类型,他包含了所需要的所有类型的特征。

interface A {
  c: string;
  d: string;
}

interface B {
  c: number;
  e: string
}

type AB = A & B;
type BA = B & A;

let a: XY; // a : { c: never , d : string, e : string}
let b: YX;//  a : { c: never , d : string, e : string}

小结:为什么 c 是 never 类型呢 ,因为不存在一种数据,又是 string 类型,又是 number 类型。

never 类型

在 TypeScript 中,never 类型表示哪些用不存在的值的类型,例如,一些总会抛出异常或根本不会有返回值的函数表达式,此外,变量也可能是 never 类型。

// 推断的返回值类型为never
function fail() {
   return error("Some error happened");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
   while (true) {}
}

// 定义never类型变量,接收返回值类型为never类型的函数返回值
let bar: never = (() => {
  throw new Error('TypeScript never');
})();

never 类型只能赋值给 never 类型,除此任意类型都不可赋值给 never 类型。

任意类型可以分解为与 never 的联合类型

type C1 = number | never;   // number
type C2 = string | never;   // string

never 类型的用途

在 TypeScript 中,我们可以利用 never 类型不能被任何类型赋值的特性来实现详细的检查。

假设我们原来有一个 people 的方法,只能接受 字符串类型和数字类型,并且根据不同的类型做不同的行为。

type Do = string | number;

function controlFlowAnalysisWithNever(do: Do) {
  if(typeof do === "string") {
    // 这里 do 被收窄为 string 类型
      console.log(`我的名字${do}`})
  } else if(typeof foo === "number") {
    // 这里 do 被收窄为 number 类型
      console.log(`我的年纪${do}`})
  } else {
    // do 在这里是 never
    const check: never = do;
  }
}

当输入为字符串或者数字类型时,一切正常,输出对应的逻辑,如果输入错误类型,会进入 never 赋值的分支,以引发报错。以此防止其他人填入错误的类型。最终目的就是使用 never 避免出现新增了联合类型而没有对应的实现,达到类型绝对安全的代码。