TypeScript 入门学习三 — TypeScript 类型系统二

168 阅读8分钟

image.png

上期学习了如何安装运行 TS 项目以及 TS 的基础类型,对象类型 这期学习 TS 剩余的类型学习完成, 希望能保持周更吧~

TypeScript 中的类型注释和类型推断

TypeScript 中的两个基本概念:类型注解类型推断,这两个概念在我们编写 TypeScript 代码时会一直使用(重点)

type annotation 类型注解

let count: number;
count = 123;

这段代码就是类型注解,意思是显示的告诉代码,我们的count 变量就是一个数字类型,这就叫做类型注解

type inferrence 类型推断

类型推断:指编程语言中能够自动推导出值的类型的能力。 它是一些强静态类型语言中出现的特性;定义时未赋值就会推论成 any 类型 如果定义的时候就赋值就能利用到类型推论

let flag; //推断为any
let count = 123; //为number类型
let hello = "hello"; //为string类型

联合类型和类型保护

只有联合类型存在的情况下,才需要类型保护,普通的类型注解,并不需要我们这种特殊操作。那先来看一下什么是联合类型

联合类型(Union Types)

所谓联合类型,可以认为一个变量可能有两种或两种以上的类型。举个🌰

interface Waiter {
  anjiao: boolean;
  say: () => {};
}

interface Teacher {
  anjiao: boolean;
  skill: () => {};
}

function judgeWho(animal: Waiter | Teacher) {
  animal.say();
}

这样写又个问题,直接写一个这样的方法,就会报错,因为judgeWho不能准确的判断联合类型具体的实例是什么。

这时候就需要再引出一个概念叫做类型保护,类型保护有很多种方法,这里统计下最常用的。

类型保护-类型断言

类型保护就是一些表达式,他们在编译的时候就能通过类型信息确保某个作用域内变量的类型 其主要思想是尝试检测属性、方法或原型,以确定如何处理值

类型断言就是通过断言的方式确定传递过来的准确值,比如上面的程序,如果会anjiao(按脚),说明他就是技师,这时候就可以通过断言animal as Teacher,然后直接调用skill方法,程序就不再报错了。同样如果不会按脚,说明就是不同的服务员,这时候调用say()方法,就不会报错了。这就是通过断言的方式进行类型保护。也是最常见的一种类型保护形式。具体看代码:

interface Waiter {
  anjiao: boolean;
  say: () => {};
}

interface Teacher {
  anjiao: boolean;
  skill: () => {};
}

function judgeWho(animal: Waiter | Teacher) {
  if (animal.anjiao) {
    (animal as Teacher).skill();
  }else{
    (animal as Waiter).say();
  }
}

类型断言有两种形式:

// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

PS: 以上两种方式虽然没有任何区别,但是尖括号格式会与 react 中 JSX 产生语法冲突,因此我们更推荐使用 as 语法。

非空断言 在上下文中当类型检查器无法断定类型时 一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型

let flag: null | undefined | string;
flag!.toString(); // ok
flag.toString(); // error

双重断言

类型断言,尽管我们已经证明了它并不是那么安全,但它也还是有用武之地。如下一个非常实用的例子所示,当使用者了解传入参数更具体的类型时,类型断言能按预期工作:

如下例子中的代码将会报错,尽管使用者已经使用了类型断言:

function handler(event: Event) {
  const element = event as HTMLElement; // Error: 'Event' 和 'HTMLElement' 中的任何一个都不能赋值给另外一个
}

如果你仍然想使用那个类型,你可以使用双重断言。首先断言成兼容所有类型的 any,编译器将不会报错:

function handler(event: Event) {
  const element = (event as any) as HTMLElement; // ok
}

TypeScript 是怎么确定单个断言是否足够

当 S 类型是 T 类型的子集,或者 T 类型是 S 类型的子集时,S 能被成功断言成 T。这是为了在进行类型断言时提供额外的安全性,完全毫无根据的断言是危险的,如果你想这么做,你可以使用 any

类型保护-in 语法

使用in语法来作类型保护,比如用if来判断animal里有没有skill()方法。

复制上面的 judgeWho()方法,改一下名称:

function judgeWhoTwo(animal: Waiter | Teacher) {
  if ("skill" in animal) {
    animal.skill();
  } else {
    animal.say();
  }
}

再举一个🌰

interface Bird {
  fly: number;
}

interface Dog {
  leg: number;
}

function getNumber(value: Bird | Dog) {
  if ("fly" in value) {
    return value.fly;
  }
  return value.leg;
}

console.log(getNumber({fly: 123}));

这里的else部分能够自动判断,得益于TypeScript的自动判断。

类型保护-typeof 语法

写一个新的add方法,方法接收两个参数,这两个参数可以是数字number也可以是字符串string,如果我们不做任何的类型保护,只是相加,这时候就会报错。代码如下:

function add(first: string | number, second: string | number) {
  return first + second;
}

解决这个问题,就可以直接使用typeof来进行解决。

function add(first: string | number, second: string | number) {
  if (typeof first === "string" || typeof second === "string") {
    return `${first}${second}`;
  }
  return first + second;
}

再举一个🌰

function double(input: string | number | boolean) {
  if (typeof input === "string") {
    return input + input;
  } else {
    if (typeof input === "number") {
      return input * 2;
    } else {
      return !input;
    }
  }
}

类型保护-instanceof 语法

如果要作类型保护的是一个对象,这时候就可以使用instanceof语法来作。现在先写一个NumberObj的类,代码如下:

class NumberObj {
  count: number;
}

然后我们再写一个addObj的方法,这时候传递过来的参数,可以是任意的object,也可以是NumberObj的实例,然后我们返回相加值,这里不进行类型保护,那这段代码会报错,如下:

function addObj(first: object | NumberObj, second: object | NumberObj) {
  return first.count + second.count;
}

使用instanceof语法进行判断一下

function addObj(first: object | NumberObj, second: object | NumberObj) {
  if (first instanceof NumberObj && second instanceof NumberObj) {
    return first.count + second.count;
  }
  return 0;
}

instanceof 只能用在类上

字面量类型

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

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

let flag1: "hello" = "hello";
let flag2: 1 = 1;
let flag3: true = true;

类型别名

类型别名用来给一个类型起个新名字

type flag = string | number;
function hello(value: flag) {}

TypeScript 提供了为类型注解设置别名的便捷语法,你可以使用 type SomeName = someValidTypeAnnotation 来创建别名:

type StrOrNum = string | number;

// 使用
let sample: StrOrNum;
sample = 123;
sample = '123';

// 会检查类型
sample = true; // Error

与接口不同,你可以为任意的类型注解提供类型别名(在联合类型和交叉类型中比较实用),下面是一些能让你熟悉类型别名语法的示例。

type Text = string | { text: string };
type Coordinates = [number, number];
type Callback = (data: string) => void;
TIP
-   如果你需要使用类型注解的层次结构,请使用接口。它能使用 `implements` 和 `extends`
-   为一个简单的对象类型(如上面例子中的 Coordinates)使用类型别名,只需要给它一个语义化的名字即可。另外,当你想给联合类型和交叉类型提供一个语义化的名称时,一个类型别名将会是一个好的选择。

交叉类型

在 JavaScript 中, extend 是一种非常常见的模式,在这种模式中,你可以从两个对象中创建一个新对象,新对象拥有着两个对象所有的功能。交叉类型可以让你安全的使用此种模式:

function extend<T extends object, U extends object>(first: T, second: U): T & U {
  const result = <T & U>{};
  for (let id in first) {
    (<T>result)[id] = first[id];
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      (<U>result)[id] = second[id];
    }
  }

  return result;
}

const x = extend({ a: 'hello' }, { b: 42 });

// 现在 x 拥有了 a 属性与 b 属性
const a = x.a;
const b = x.b;

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

interface IPerson {
  id: string;
  age: number;
}

interface IWorker {
  companyId: string;
}

type IStaff = IPerson & IWorker;

const staff: IStaff = {
  id: 'E1006',
  age: 33,
  companyId: 'EFT'
};

console.dir(staff)

小结

TypeScript 的类型系统学到这里~

参考文章

最全的TypeScript学习指南
技术胖的 TypeScript免费视频图文教程(2W字)
TS的对象类型、数组类型、函数类型 掌握 tsconfig.json
深入理解 TypeScript
1.2W字 | 了不起的 TypeScript 入门教程

TS 推荐学习网站

TypeScript 中文手册
技术胖 - TypeScript 从入门到精通图文视频教程-免费教程
深入理解 TypeScript
TypeScript 官方提供的在线 TypeScript 运行环境

如果大佬还知道更多的学习网站,欢迎留言~