概述
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
}
}