【TypeScript】阮一峰TypeScript教程笔记:基本用法、any 类型等、类型系统

是阮一峰 TypeScript 教程的笔记。笔记目录与教程目录一致。笔记不全,仅作自己学习使用,完整学习请看:阮一峰 TypeScript 教程或TypeScript官方文档。

文章目录

    • 简介
    • 基本用法
    • any 类型,unknown 类型,never 类型
      • any 类型
      • 类型推断问题
      • 污染问题
      • unknown 类型
      • never 类型
    • 类型系统
      • 基本类型
      • 包装对象类型
      • 包装对象类型与字面量类型
      • Object 类型与 object 类型
      • undefined 和 null 的特殊性
      • 值类型
      • 联合类型
      • 交叉类型
      • type 命令
      • typeof 运算符
      • 类型的兼容

简介

TypeScript 可以看成是 JavaScript 的超集(superset),即它继承了后者的全部语法,所有 JavaScript 脚本都可以当作 TypeScript 脚本(但是可能会报错),此外它再增加了一些自己的语法。

TypeScript 对 JavaScript 添加的最主要部分,就是一个独立的类型系统

类型系统

类型是人为添加的一种编程约束和用法提示。 目的:提早发现错误。

举个例子:

function addOne(n: number) {
  return n + 1;
}

若:addOne("hello");,在TypeScript中会报错,但在JS中不会。

动态类型与静态类型

JS是动态类型语言,TS是静态类型语言。

// 例一
let x = 1;
x = "hello";

// 例二
let y = { foo: 1 };
delete y.foo;
y.bar = 2;

若在JS中,上面的代码就是正确的:约束性很弱,不利于提前发现代码错误。(无法提前知道某个属性在不在,或某个变量的数据类型
在TS中,上述代码会报错。TS为JS引入了静态类型特征

基本用法

类型声明

为JS变量加上类型声明。写法:标识符后“冒号+类型”。

变量:

let foo:string;

函数:

function toString(n: number): string {
    return String(n);
}

报错:

  • 变量的值与声明类型不一致
  • 变量未赋值就使用(在JS中,为赋值的变量返回undefined但不会报错)

类型推断

类型声明并不是必需的。若无,TS会自己推断类型。若变量赋值后更改为其他类型的值,跟推断的类型不一致,TS会报错。

let foo = 123;
foo = "hello"; // 报错

编译

JS的运行环境(浏览器和Node.js)不认识TS。因此,TS想要运行,要先转为JS,这个过程即编译

关于编译:编译时,会将类型声明和类型相关的代码全部删除,只留下能运行的 JavaScript 代码,并且不会改变 JavaScript 的运行结果。

关于类型检查:只是编译时的类型检查,而不是运行时的类型检查。

值与类型

“类型”是针对“值”的,可以视为是“值”的一个属性。每一个值在TS中都是有类型的。如,3是一个值,他的类型是number。

TypeScript 代码只涉及类型,不涉及值。所有跟“值”相关的处理,都由 JavaScript 完成。
这一点务必牢记。TypeScript 项目里面,其实存在两种代码,一种是底层的“值代码”,另一种是上层的“类型代码”。前者使用 JavaScript 语法,后者使用 TypeScript 的类型语法。

在TS的编译过程,其实就是把“类型代码”全部拿掉,只保留“值代码”。

TypeScript Playground

https://www.typescriptlang.org/play

any 类型,unknown 类型,never 类型

any 类型

any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。

let x: any;

x = 1; // 正确
x = "foo"; // 正确
x = true; // 正确
let x: any = "hello";

x(1); // 不报错
x.foo = 100; // 不报错

上面示例中,变量x的值是一个字符串,但是把它当作函数调用,或者当作对象读取任意属性,TypeScript 编译时都不报错。原因就是x的类型是any,TypeScript 不对其进行类型检查。

因此,尽量避免使用any类型,否则就失去了使用 TypeScript 的意义。

实际开发中,any类型主要适用以下两个场合:

  1. 出于特殊原因,需要关闭某些变量的类型检查,就可以把该变量的类型设为any。
  2. 为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为any。有些年代很久的大型 JavaScript 项目,尤其是别人的代码,很难为每一行适配正确的类型,这时你为那些类型复杂的变量加上any,TypeScript 编译时就不会报错。

类型推断问题

对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是any

使用letvar命令声明变量,但不赋值也不指定类型,是不会报错的。

var x; // 不报错
let y; // 不报错

建议使用let和var声明变量时,如果不赋值,就一定要显式声明类型,否则可能存在安全隐患。 如下面代码:用let声明后不赋值,则x类型推断为any。下面代码不会报错。

let x;

x = 123;
x = { foo: "hello" };

污染问题

any类型除了关闭类型检查,还有一个很大的问题,就是它会 “污染” 其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。

let x: any = "hello";
let y: number;

y = x; // 不报错

y * 123; // 不报错
y.toFixed(); // 不报错

上面示例中,变量x的类型是any,实际的值是一个字符串。变量y的类型是number,表示这是一个数值变量,但是它被赋值为x,这时并不会报错。然后,变量y继续进行各种数值运算,TypeScript 也检查不出错误,问题就这样留到运行时才会暴露。

污染其他具有正确类型的变量,把错误留到运行时,这就是不宜使用any类型的另一个主要原因。

unknown 类型

为了解决any类型“污染”其他变量的问题,TypeScript 3.0 引入了unknown类型。它与any含义相同,表示类型不确定,可能是任意类型,但是有一些限制。

与any的相同之处:所有类型的值都可以分配给unknown类型。
与any的不同之处:不能直接使用。

  • 不能直接赋值给其他类型的变量(除了any类型和unknown类型)
  • 不能直接调用unknown类型变量的方法和属性
  • unknown类型变量能进行的运算有限:比较运算(运算符=====!=!==||&&?)、取反运算(运算符!)、typeof运算符和instanceof运算符这几种,其他运算都会报错

使用unknown的方法:“类型缩小”。即缩小unknown变量的类型范围,确保不会出错。

如:

let a: unknown = 1;

if (typeof a === "number") {
  let r = a + 10; // 正确
}
let s: unknown = "hello";

if (typeof s === "string") {
  s.length; // 正确
}

unknown可以看作是更安全的any。凡是需要设为any类型的地方,通常都应该优先考虑设为unknown类型。

never 类型

TS引入“空类型”的概念,即该类型为空,不包含任何值。由于不存在任何属于“空类型”的值,所以该类型被称为never,即不可能有这样的值。

let x: never;

上面示例中,变量x的类型是never,就不可能赋给它任何值,否则都会报错。

使用场景:

  • 在一些类型运算之中,保证类型运算的完整性
  • 不可能返回值的函数

如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于never类型。如:

function fn(x: string | number) {
  if (typeof x === "string") {
    // ...
  } else if (typeof x === "number") {
    // ...
  } else {
    x; // never 类型
  }
}

可以赋值给任意其他类型。

function f(): never {
  throw new Error("Error");
}

let v1: number = f(); // 不报错
let v2: string = f(); // 不报错
let v3: boolean = f(); // 不报错

类型系统

基本类型

boolean、string、number、bigint、symbol、object、undefined、null。

undefined:表示未定义。
null:表示空(此处没有值)。

包装对象类型

booleanstringnumber有对应的包装对象类型:Boolean()String()Number()

在调用方法时,字符串会自动转为包装对象。(原本原始类型的值没有方法,对象才有)

"hello".charAt(1); // 'e'

当作构造函数使用时,才会返回包装对象。

const s = new String("hello");
typeof s; // 'object'
s.charAt(1); // 'e'

包装对象类型与字面量类型

每一个原始类型的值都有包装对象和字面量两种情况。

"hello"; // 字面量
new String("hello"); // 包装对象

大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象:

const s1: String = "hello"; // 正确
const s2: String = new String("hello"); // 正确

const s3: string = "hello"; // 正确
const s4: string = new String("hello"); // 报错

建议只使用小写类型,不使用大写类型。

因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。而且,TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错。如:

const n1: number = 1;
const n2: Number = 1;

Math.abs(n1); // 1
Math.abs(n2); // 报错

内置的Math.abs的参数类型是小写的number,若传入大写的Number类型就会报错。

Object 类型与 object 类型

Object(大写)

所有可以转成对象的值,都是Object类型。除了undefinednull这两个值不能转为对象,其他任何值都可以赋值给Object类型。

空对象{}Object类型的简写形式。

object(小写)

不包含原始类型值,只包含对象、数组和函数。

let obj: object;

obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;
obj = true; // 报错
obj = "hi"; // 报错
obj = 1; // 报错

建议总是使用小写类型object。大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。

undefined 和 null 的特殊性

任何其他类型的变量都可以赋值为undefined或null。

let age: number = 24;

age = null; // 正确
age = undefined; // 正确

这样做的原因是:以便跟 JavaScript 的行为保持一致。

有时候,这不是开发者想要的行为,也不利于发挥类型系统的优势。

const obj: object = undefined;
obj.toString(); // 编译不报错,运行就报错

为了避免这种情况,TypeScript 提供了一个编译选项strictNullChecks。打开它以后,undefinednull只能赋值给自身,或者any类型和unknown类型的变量。

值类型

单个值也是一种类型,称为“值类型”。值类型不能赋为其他值。

let x: "hello";

x = "hello"; // 正确
x = "world"; // 报错

注意,const命令声明的变量,如果赋值为对象,并不会推断为值类型。const变量赋值为对象时,属性值是可以改变的。

联合类型

联合类型(union types)指的是多个类型组成的一个新类型,使用符号|表示。联合类型A|B表示,任何一个类型只要属于A或B,就属于联合类型A|B。

let x: string | number;

x = 123; // 正确
x = "abc"; // 正确

联合类型可以与值类型相结合,表示一个变量的值有若干种可能。

let setting: true | false;

let gender: "male" | "female";

let rainbowColor: "赤" | "橙" | "黄" | "绿" | "青" | "蓝" | "紫";

前面提到,打开编译选项strictNullChecks后,其他类型的变量不能赋值为undefined或null。这时,如果某个变量确实可能包含空值,就可以采用联合类型的写法。

let name: string | null;

name = "John";
name = null;

如果一个变量有多种类型,读取该变量时,往往需要进行“类型缩小”(type narrowing),区分该值到底属于哪一种类型,然后再进一步处理。

直接调用会报错:

function printId(id: number | string) {
  console.log(id.toUpperCase()); // 报错
}

类型缩小:

function printId(id: number | string) {
  if (typeof id === "string") {
    console.log(id.toUpperCase());
  } else {
    console.log(id);
  }
}

“类型缩小”是 TypeScript处理联合类型的标准方法,凡是遇到可能为多种类型的场合,都需要先缩小类型,再进行处理。实际上,联合类型本身可以看成是一种“类型放大”(type widening),处理时就需要“类型缩小”(type narrowing)。

交叉类型

交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&表示。任何一个类型必须同时属于A和B,才属于交叉类型A&B,即交叉类型同时满足A和B的特征。

主要用途是表示对象的合成。 变量obj同时具有属性foo和属性bar。

let obj: { foo: string } & { bar: string };

obj = {
  foo: "hello",
  bar: "world",
};

常常用来为对象类型添加新属性。 类型B是一个交叉类型,用来在A的基础上增加了属性bar。

type A = { foo: number };
type B = A & { bar: number };

type 命令

type命令用来定义一个类型的别名。

type命令为number类型定义了一个别名Age。这样就能像使用number一样,使用Age作为类型。

type Age = number;
let age: Age = 55;

别名的作用域是块级作用域。

if (true) {
  type T = number;
  let v: T = 5;
} else {
  type T = string;
  let v: T = "hello";
}

别名支持使用表达式,也允许嵌套。

示例中,别名Greeting使用了模板字符串,读取另一个别名World。

type World = "world";
type Greeting = `hello ${World}`;

typeof 运算符

JS中typeof运算符返回的都是字符串:

typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 1337; // "number"
typeof "foo"; // "string"
typeof {}; // "object"
typeof parseInt; // "function"
typeof Symbol(); // "symbol"
typeof 127n; // "bigint"

TS中返回的是TS类型:

const a = { x: 0 };

type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number

同一段代码可能存在两种typeof运算符,一种用在值相关的 JavaScript 代码部分,另一种用在类型相关的 TypeScript 代码部分。如:

第一个是类型运算,第二个是值运算。

let a = 1;
let b: typeof a;

if (typeof a === "number") {
  b = a;
}

JavaScript 的 typeof 遵守 JavaScript 规则,TypeScript 的 typeof 遵守 TypeScript 规则。它们的一个重要区别在于,编译后,前者会保留,后者会被全部删除。上例的代码编译结果如下。

let a = 1;
let b;
if (typeof a === "number") {
  b = a;
}

TypeScript 规定,typeof 的参数只能是标识符,不能是需要运算的表达式。typeof命令的参数不能是类型。

类型的兼容

如果类型A的值可以赋值给类型B,那么类型A就称为类型B的子类型(subtype)

如:类型number就是类型number|string的子类型。

type T = number | string;

let a: number = 1;
let b: T = a;

凡是可以使用父类型的地方,都可以使用子类型,但是反过来不行。

let a: "hi" = "hi";
let b: string = "hello";

b = a; // 正确
a = b; // 报错

你可能感兴趣的:(TypeScript,typescript,笔记,ubuntu)