TypeScript 基础知识

115 阅读7分钟

概述

TypeScript 是 JavaScript 超集

  • 语法上,TypeScript 包含了 JavaScript 语法
  • 最终运行时,TypeScript 不会改变 JavaScript 行为

TypeScript (tsc) 主要是为 JavaScript 提供了静态类型检测和代码生成

  • 静态类型检测:在程序执行前,对变量的静态类型进行检测,如果没有通过类型检测,那么 TypeScript 就会报出相应的错误

注:这不是在运行时触发的!运行时所有类型都会被去除,因此类型对运行时的性能没有影响

  • 代码生成:在代码生成中,TypeScript 是不会因为类型而生成失败,因为它是完全独立的,和类型没有关系,不会改变 JavaScript 的运行,如果生成失败,那就是程序逻辑的问题

类型检测不通过,但是不会影响代码转译

// --- ./index.ts ---
let a = 1;
a = ''; // 不能将类型“""”分配给类型“number”

// tsc index.ts

// --- index.js ---
let a = 1;
a = '';

除非开启 noEmitOnError 选项,才会在有类型错误的时候停止生成

tsc --noEmitOnError index.ts

【转译:是指同级别语言生成等效源代码的过程,编译是指高等级语言转换成低等级语言】

类型系统

当全局安装 typescript 后,会得到两个可执行文件

  • tsc:typescript 编译器,可以手动生成 JS 代码
  • tsserver:typescript 独立服务器,它会为编辑器或者 IDE 提供语言服务,包括自动补全、检查、导航、重构等等

类型可以看作是所有可能值的集合,这个集合被称为类型的域

  • number 就是 1、2、3 数字的集合,而"Hello World"不属于这个集合
  • s 不属于 sType 类型的集合
type sType = 'left' | 'right';
let s: sType = '1';

那么既然有域,就有范围,所以就存在类型之间的关系 如:

  • 类型 number 就是 Number 的子集
  • &操作符就是两个集合的交集
  • |操作符就是两个集合的并集

日常类型

JS 基本类型

boolean、bigint、number、string、symbol、 null 和 undefined

let a: string = '';
let a: String = 'a'; // 不要使用包装类,包装类域的范围大于基本类型
['a', 'b'].includes(a); // 报错:类型“String”的参数不能赋给类型“string”的参数

null 和 undefined 可以通过 strictNullChecks 属性来管理,如果开启就必须判断

引用类型

object、Array、Function

any: 允许任何内容,即无视类型检查

  • 任何类型可以分配给 any,any 也可以分配给任何类型
let a: any = 1;
a = ''; // 没有报错

let b: number = 1;
a = '' as any; // 没有报错
  • 可以通过 noImplicitAny 配置进行关闭,禁止写 any 类型

unknown: 当不知道是什么类型的时候,强制类型推断

  • 任何类型可以分配给 unknown,但是 unknown 不可以分配给任何类型

    let a: unknown = 1;
    a = ''; // 没有报错
    
    let a = 1;
    a = '' as unknown; // 不能将类型“unknown”分配给类型“number”
    

never: 空集,最底层类型

  • never 可以分配给任何类型,但是任何类型可以分配给 never(和 unknown 刚好相反)
let a = 1;
let b: never = a; // 不能将类型“number”分配给类型“never”
a = '' as never; // 没有报错
  • 两个类型集合没有交集那就是 never
// 两个类型集合没有交集那就是never
type K = number & string; // never

void

作为函数返回 undefined 或不返回的声明

元组类型(tupln)

允许表示一个已知元素数量和类型的数组

let arr: [number, string, number, number] = [1, 'hello', 2, 3];

声明&断言

  • 类型声明会走 ts 正常的额外属性检查(针对字面量赋值给已声明类型的变量)
  • 断言就是告诉 ts 不做检查,强制指定了这个类型
interface Person {
  name: string;
}
const person1: Person = {}; // Property 'name' is missing in type '{}' but required in type 'Person'
const person2 = {} as Person; // 断言为Person类型,没有错误
const person3 = <Person>{}; // 断言为Person类型,没有错误

但是当自己知道类型而 TS 不知道时,可以使用以下方式作为类型断言,所有类型都是 unknown 的子集,同时也是显式的标记了可能存在问题的地方

const person = (who as unknown) as Person;

额外属性检查

以下情况会触发 Typescript 额外属性检查,即检查(对象字面量)没有除类型指定外的其他属性

  • 对象字面量赋值给一个(已声明类型的)变量
  • 对象字面量作为参数传递给(参数已声明类型的)函数
interface Person {
  name: string;
}
const person1: Person = {
  name: 'Dilomen',
  age: 27, // 不能将类型“{ name: string; age: number; }”分配给类型“Person”。 对象文字可以只指定已知属性,并且“age”不在类型“Person”中
};
const person2 = {
  name: 'Dilomen',
  age: 27,
};
// 可以引入一个额外的变量来消除检查
const person3: Person = person2; // 没有错误

联合&交叉类型

联合类型 |操作符就是两个集合的并集

type a = {
  name: string;
};
type b = {
  age: number;
};

type c = a | b;
let d: c = { name: 'dilomen' }; // 因为是并集,所以满足一方即可

交叉类型 &操作符就是两个集合的交集

type a = {
  name: string;
};
type b = {
  age: number;
};

type c = a & b;
let d: c = { name: 'dilomen', age: '27' }; // 因为是交集,所以必须满足两个集合的所有属性

这里的并集交集要拿类型作为集合来看,而不是看属性

type 和 interface

  • type 可以支持联合类型、而且更容易表达数组和元组类型
  • interface 可以支持声明合并
  • 除了以上两个必选外,其余场景基本两者都能满足,还是需要按照项目规范定义来选择
interface IPerson {
  name: string;
}

interface IPerson {
  age: number;
}

const person: IPerson = {
  name: 'Dilomen',
  age: 27,
};

类型推断

当 TypeScript 能够推断出类型的时候,可以不用写类型标注

let str1: string = ''; // ❌不推荐
let str2 = ''; // ✅推荐
const str3 = 'a'; // ✅推荐,能够更准确的推断出类型为'a',而不是string

对象字面量和函数返回必须使用显示标注,这样才能让错误提示显示在正确位置上

type dataType = { id: number };
type logFnType = (data: dataType) => {};

const log: logFnType = () => {};

const data = {};
log(data); // Property 'id' is missing in type '{}' but required in type 'dataType'.

// 能够将错误正确显示在值的位置,而不是使用方的位置
const data: dataType = {}; // Property 'id' is missing in type '{}' but required in type 'dataType'.
log(data);

可以借助 eslint 的 no-inferrable-types 规则来帮助确保写的所有类型标注都是真正必要的

一个变量的值可以改变,但是它的类型只有一个,所以,如果当改变的值不符合当前指定的类型时,最好另起一个变量,避免重复使用不同类型值(如联合类型)的变量而带来类型检测问题。

function fn(arg: number | string) {}
let a = 1;
fn(a);
a = '1'; // ❌不推荐
fn(a);
let b = '1'; // ✅推荐
fn(b);

类型扩展

  • 当没有对变量进行类型标注时,没有上下文,因此 TypeScript 会根据当前值判断出一组可能的值,即类型像父级集合进行扩展
function fn(arg: 'x' | 'y' | 'z') {}
// a的值被类型扩展成 string
let a = 'x';
fn(a); // 类型“string”的参数不能赋给类型“"x" | "y" | "z"”的参数。
const arr = ['x', 1]; // (string | number)[]
// 而不是('x', 1)[]、['x', 1]、[string, number]...

去除类型拓展的方式

  • 添加类型标注
  • 直接通过值传递,如 fn('x')
  • 可以使用 const 来避免这种类型扩展
const a = 'x';
const arr = ['x', 1] as const; // readonly ["x", 1]

类型收缩

  • 需要在现有的类型集合中缩小范围
// 可以通过JS的条件来缩小类型的范围
const el: HTMLElement | null = xxxx;
if (el) {
  el; // HTMLElement
} else {
  el; // null
}

使用别名时,后续所有要保持一致

interface infoType {
  name: string;
  age: number;
}
interface personType {
  info: infoType | number;
}

function test(person: personType) {
  // 如果使用了别名,后续使用就要保持一致,因为它可能会变成不同于原先属性下的值,即和原先的属性值类型脱钩
  let info = person.info;

  if (typeof info !== 'number') {
    person.info; // infoType | number
    info; // infoType
  }

  if (typeof person.info !== 'number') {
    person.info; // infoType
    info; // infoType | number
  }
}