TypeScript 的基础概要

  • 简介:

    • TypeScript 由微软开发,是基于 JavaScript 的一个扩展语言,它包含了 JavaScript 的所有内容,所以它被称为 JavaScript 的超集
    • TypeScript 增加了静态类型检测接口泛型等诸多现代化开发特性,更适合大型项目的开发。
    • TypeScript 不能直接运行在浏览器中,它需要经过编译后转为JavaScript才能在浏览器中运行。
    • 由于方便书写,下文中的一系列TypeScript我将都简称为TS
  • 为何需要:

    • JavaScript 的出现

      • 由 JavaScript 最初的浏览器脚本语言为引,在后续的发展中,JavaScript 能做的事情越来越多,截至今日亦可以开发全栈项目
      • 随着 JavaScript 的流行和使用,不同项目也随着应用场景的开发堆叠越来越多的 JavaScript 代码,这使得不同的开发人员在此基础上开发维护显得异常艰难。
    • JavaScript 的困扰

      • 不清楚的数据类型

        const str = "a";
        str(); // 类型错误,显示str不是一个function
      • 有问题的逻辑交互

        const value = Math.floor(Math.random() * 100) % 2 ? "偶数" : "奇数";

        // 逻辑错误,重叠
        if (value !== "奇数") {
        console.log("不等于奇数");
        } else if (value === "偶数") {
        console.log("它是偶数");
        }
      • 属性的不安全访问

        const person = {
        name: "Alan",
        age: 18,
        };

        console.log(
        `我是${person.name},今年${person.age},身高${person.height}`
        ); // 警告,person不存在height属性
      • 代码词汇的拼写错误

        const java = "java 是一门后端开发语言";

        console.log(jvav); // 低级错误,手快敲错

静态类型检查

  • TS会在代码运行前进行检查,能够发现文件中代码的错误和不合理之处,减小运行时出现的 bug 几率,这种行为被称为静态类型检查
  • 虽然在同样的模块中,TS 的代码量会比 JavaScript 的多得多,但是基于 TS 带来的各种类型规范,恰好能够使得 TS 代码中的代码结构更加清晰,清晰明了的类型也使得开发者在后期的项目维护中更得心应手。

TS 编译

  • 终端命令行编译
    • 安装 typescript
      npm install -g typescript
    • 使用 tsc 命令并指定编译文件
      tsc /path/to/ts/file
  • 自动化编译
    • 同理也是使用tsc进行编译,但区别是我们需要用到额外的参数进行编译
    • 初始化 tsconfig.json 配置文件,具体参数请参考官方文档
      tsc --init
    • 终端运行指令,实现 ts 文件热更新编译 JavaScript
      tsc --watch /path/to/ts/file

类型声明

  • 变量或函数形参类型声明:
let str: string;
let num: number;
let bool: boolean;

//变量在声明时已经给定类型,在变量值变更过程中,无法使用已声明好的类型外的值
str = "asd"; // ✅
num = 234; // ✅
bool = false; // ✅

// 错误赋值将会引起TS的类型检查错误提示.[不能将类型xxx分配给类型xxx]
str = 123; // ❌
num = true; // ❌
bool = "bool"; // ❌

// 声明一个函数fn,两个入参分别为number和string类型吗,函数需要返回一个string类型的返回值
function fn(a: number, b: string): string {
return a + b;
}
  • 字面量类型:字面量类型是一种类型,它表示某个特定的值,而不是某一类值。
let str: "hello";

str = "asd"; // ❌ 警告,ts会把变量冒号后的字符串当作为类型来推断,当你在改变变量值时,无法匹配字面量

function fn(): 1 | 2 | 3 {} // 函数只能返回1、2、3中的其中一个

类型推论

  • 在 TS 里,有些没有明确指出类型的地方,它会自动类型推论帮助使用者提供正确的类型。
let a = 23;

a = true; // ❌ 警告,因为ts会推断初始变量值的类型,并限制它后续的一系列变更操作

let obj = {};

obj = [1, 2, 3]; // ts不会推断未声明类型的复杂数据类型

TypeScript 中的基础类型

  • 小写类型和首字母大写类型含义不一样,不能一概使用

  • 小写(string)是原始类型,首字母大写(String)是包装对象类型

  • 我们基本不推荐使用包装对象类型进行类型声明

    • 它是可变的引用类型,具有额外的属性和方法,会引入不必要的复杂性,例如原型链上的不确定性属性带来的影响
    • 使用包装对象会创建额外的对象,带来一定程度上的内存消耗性能开销,使用原始类型相对来说会更轻量、可读性更强、效率更高
    • 增加代码复杂性,不利于代码的维护
    • 自动装箱
      • 此概念是 TS 中所存在的,它解释了包装对象类型存在的意义
    const str = "this is a string"; // 原始类型

    console.log(str.length);

    // mock js engin compiler
    const str = "this is a string";
    const size = (function () {
    // 自动装箱,将会创建一个临时的包装对象包装原始类型值,例如这里的String
    let tempObject = new String(str);
    // 访问包装对象上的属性(prototype),如length
    let tempValue = tempObject.length;
    // 销毁临时对象,返回对应值
    // js引擎会自动回收对象销毁(V8垃圾回收),用户手动回收释放内存可以设置属性为null,tempObject = null

    return tempValue;
    })();

    console.log(size);

JavaScript 标准内置类型

string/String(略)

number/Number(略)

boolean/Boolean(略)

bigInt(略)

symbol(略)

undefined(略)

null(略)

object/Object

  • 较特殊,包含 Array,Function,Date,Error 等诸多对象

    无论是小写object还是大写Object,在实际开发中都使用较少,因为其包含范围比较广

    • object小写含义:所有非原始类型,包含对象数组函数等,范围极其广泛

    • 对象类型声明

    let obj: object;

    obj = []; // ✅
    obj = {}; // ✅
    obj = () => {}; // ✅
    obj = new Error("123");
    class Mock {} // ✅
    obj = new Mock(); // ✅

    // 如果将原始类型赋值给object
    obj = 123; // ❌,警告,不能将类型number分配给object
    obj = "123"; // ❌,警告,不能将类型string分配给object
    obj = true; // ❌,警告,不能将类型boolean分配给object
    obj = undefined; // ❌,警告,不能将类型undefined分配给object
    obj = null; // ❌,警告,不能将类型null分配给object
    • 数组类型声明
    // 两种书写格式皆可
    let arr: number[]; // ✅,数组元素皆为number
    arr = [1, 2, 3]; // ✅
    arr = [1, 2, "123"]; // ❌

    let arr2: Array<string>; // ✅泛型,数组元素皆为string
    • 函数类型声明
    // 函数类型声明表达式, => 在这里是表达分隔符
    // 特殊情况下的函数声明(type类型声明),会对函数的返回值有一定影响,具体可以参考type目录中的特殊情况
    let computed: (x: number, b: number) => number;
    computed = (a, b) => a + b;
    computed(1, 2);

TypeScript 自带类型

tuple

  • 元组(tuple)是一种特殊的数组类型,用于精确描述一组值的类型。可以存储固定数量的元素,每个元素的类型是已知的且可以不同。有时候可以使用?代表可选值。
let arr: [number, string, boolean, ?number];
arr = [1, "123", true, 123]; // ✅
arr = [1, "123", true]; // ✅,第四个元素代表可选,可以不添加
arr = [1, 2, 3]; // ❌,需要精确匹配到声明的类型,这里的是 [number, string, boolean]

let arr2: [number, boolean, ...string]; // 拓展语法,表示包含第三个元素后的所有元素都是string类型
arr2 = [1, false, "1", "2", "3"]; // ✅

enum

  • 枚举(enum)可以定义一组命名常量,作用在于增强代码的可读性,方便代码的维护

数字枚举(具有递增性),默认从 0 开始,如果更改枚举 number 值,将会自动重新从小到大排序

enum Actions {
Up,
Down,
Left,
Right,
}

// 输出:具有反射特征的一个object {0: "Up", 1: "Down", 2: "Left", 3: "Right", Up: 0, Down: 1, Left: 2, Right: 3}
console.log(Actions);
// 访问枚举值
console.log(Actions.Up);
console.log(Actions[0]); // 不常用

字符串枚举

  • 不使用枚举
function getSkills(skill) {
switch (skill) {
case "Frontend":
return "HTML, CSS, JS";
case "Backend":
return "Node, Python, Java";
case "Mobile":
return "React Native, Flutter";
case "Devops":
return "AWS, Docker, Kubernetes";
default:
return "Invalid skills";
}
}
getSkills("Frontend"); // 笨拙,且编译器不会提示,且可能会输错枚举导致函数输出错误结果
  • 使用枚举后
enum Skills {
Frontend = "Frontend",
Backend = "Backend",
Mobile = "Mobile",
DEVOPS = "Devops",
}

function getSkills(skill: Skills): string {
switch (skill) {
case Skills.Frontend:
return "HTML, CSS, JS";
case Skills.Backend:
return "Node, Python, Java";
case Skills.Mobile:
return "React Native, Flutter";
case Skills.DEVOPS:
return "AWS, Docker, Kubernetes";
default:
return "Invalid skills";
}
}
// ts会在你选择枚举值时给予提示,开发只需关心枚举的值变更和逻辑实现即可,不再需要关心是否输错内容
getSkills(Skills.Backend);

常量枚举(优化手段):它是一种特殊枚举值,使用const关键字定义,在编译时会被内联,可以避免产生额外的代码

  • 编译内联指的是 TS 在编译时,会将枚举成员的引用替换为其对应的实际值,而不是生成额外的枚举对象,这样可以实际减少编译后js代码的输出,提高运行时性能
  • 当使用了常量枚举声明后,在使用时不引用具体的枚举值,TS 会警告"const" 枚举仅可在属性、索引访问表达式、导入声明的右侧、导出分配或类型查询中使用。
  • 默认情况下的 ts 编译后枚举结构
enum Actions {
Up,
Down,
Left,
Right,
}
// ts编译后的js结构(输出)
var Actions;
(function (Actions) {
Actions[(Actions["Up"] = 0)] = "Up";
Actions[(Actions["Down"] = 1)] = "Down";
Actions[(Actions["Left"] = 2)] = "Left";
Actions[(Actions["Right"] = 3)] = "Right";
})(Actions || (Actions = {}));
  • 使用常量枚举后的结构
const enum Actions {
Up,
Down,
Left,
Right,
}
// 如果枚举值没有被引用,则不会编译输出内容
// 如果被引用,则会输出其引用的实际值
console.log(Actions.Up); // js => console.log(0 /* Actions.Up */);

any

  • 不推荐使用 : TS 将会忽略类型检查,允许用户随意更改属性/方法的输入输出,即所谓的“AnyScript”,令人贻笑大方。

void

  • 常用于函数返回值的声明,它是没有任何意义的返回值,一个void类型的变量没有什么大用,因为你只能为它赋予undefined
  • 在 Js 中,函数默认情况下会返回undefined,但是在 TS 中会给没有定义具体返回类型(默认返回值)的函数推导出void,所以很多情况下可以忽略void的类型声明,
  • 注意void作为函数返回值时不应当被使用者依赖其返回值进行任何操作,因为前面也说了,它是没有任何意义的返回值
  • 相较于 Js 中的undefined,TS 中的void是更加严格的一种表示的类型,且语义上也表示不关心该返回值,它的返回值都不应该被使用或依赖(重要的事情再说一遍!)
type undef = undefined;
function fn1(): undef {}
// 等同于
function fn2(): void {}

// 示例:以下三种写法等价
// 1. 基础函数
function noReturn(): void {} // ✅
// 2. 带return的函数
function haveReturn(): void {
return; // ✅
}
// 3.带返回值的函数
function haveReturnValue(): void {
return undefined; //✅
}

// void与undefined的差异!!!
function voidFunction(): void {
return; // 或者 return undefined;
}

function undefinedFunction(): undefined {
return undefined;
}

// ✅,可以使用,因为undefinedFunction函数使用的是具体的undefined类型返回值
if (undefinedFunction()) {
console.log("use in fn1");
}

// ❌,警告,无法测试 void 类型的表达式的真实性。
// 因为voidFunction函数中void的undefined代表的是无意义的“空”状态,且并不能被使用或依赖
if (voidFunction()) {
console.log("use in fn2");
}

never

  • never类型表示的是那些永不存在的值的类型。用作于函数的返回值或者某种情况下由 TS 检查自动推断出来。通常也用作于完整性/边界性检查。
// 某种条件语句,让ts主动推断
let s: string;
s = "dasd";

if (typeof s === "string") {
console.log(s);
} else {
console.log(s);
}
  • 是那些总是会抛出异常或根本就不会有返回值函数表达式箭头函数表达式的返回值类型(简称:函数返回值类型)
// 符合never类型的情况,在函数中的表现主要有以下几种:
// 1、函数无法顺利的调用结束,函数不能正常返回值,例如函数默认值返回值 undefined
function fn(): never {
throw new Error("123");
}
// 2、函数永远不会停止调用(递归)
function fn2(): never {
fn2();
}
// or
function infiniteLoop(): never {
while (true) {}
}

unknown

  • 语义化理解,未知类型。可以理解为一个安全的any类型,TS 会对该类型的值进行检查,适用于不确定数据的具体类型。

  • any比较:

    • 强制类型检查
    // any 类型,不管变量被如何更改,ts都不会进行类型检查
    let a: any;
    a = 1;
    a = "123";
    a = false;
    a = {};

    // unknown 类型,与any一样可以随意赋值,但是无法将该unknown类型的变量赋值给声明具体类型的变量,因为TS会对其进行类型检查
    let num: number;
    let un: unknown;
    un = 123;
    un = "123";
    un = false;

    num = un; // ❌,TS将会警告提醒,不能将unknown类型分配给number

    // 可以限制条件使得ts检查通过,代价是不必要的代码量
    if (typeof un === "number") {
    num = un;
    }

    // 甚至可以使用`断言(as)`指定类型
    num = un as number;
    //or
    num = <number>un; // 断言的另一种写法
    • 读取任何属性、对象原型方法都会警告
    // any类型下的所有属性都不会警告,因为ts不检查
    let an: any;
    an = 123;
    an = "3213";
    an.toUpperCase();
    an.bool = false;

    // unknown类型则会受到一定限制
    let un: unknown;
    un = "123";
    un.toLowerCase(); // ❌,ts警告,un的属性是未知的
    un.a = 1; // ❌,皆会警告
    un.b = "213"; // ❌,皆会警告
  • 元组

用于定义类型的关键字

type

type能为任意类型创建别名,让代码更加简洁优雅、可读性更强,且也能更方便的进行类型复用和扩展

  • 类型别名:
type newString = string;
type newNumber = number;
type newBoolean = boolean;

const bool: newBoolean = false;
const newNumber: newNumber = 1;
const str: newString =
"类型别名,通过type可以定义一个新的类型名称,在使用时引用";
  • 联合类型:高级类型用法,表示一个值可以为多种不同类型之一
type commonType = number | string | boolean;

let value: commonType =
"使用联合类型,可以让你的变量具有可选性,在这里的commomType可以让你的变量在变更时可以在number、string、boolean中选择";
value = 1; // ✅
value = false; // ✅
value = []; // ❌ ,警告,不能将类型“never[]”分配给类型“commonType”。
  • 交叉类型:可以将多个类型合并成一个,合并后的类型拥有所有被合并类型的成员。常用于对象类型
type Animal = {
eyes: string;
hair: string;
fangs: string;
claws: string;
};

type Lion = {
sex: string;
age: number;
isIll: boolean;
};

type Actions = {
jump: boolean;
flutter: boolean;
fly?: boolean;
};

type LionActions = Animal & Lion & Actions;

let lion: LionActions;
// 变量具有合并后的所有属性,(?)属性表示可选
lion = {
eyes: "red",
hair: "yellow",
fangs: "sharp",
claws: "long",
sex: "boy",
age: 12,
isIll: false,
jump: true,
flutter: false,
};
  • 不应该在交叉类型中使用基本数据类型,因为 TS 无法推断出你的值,默认返回never
type AnyValue = string & number;
let value: AnyValue; // ❌,TS无法推断
特殊情况

通过类型声明限制函数返回值为 void 时,TS 将不会严格要求函数返回值为空

type VoidFn = () => void;

let fn: VoidFn = () => 123; // 不会警告
let fn2: VoidFn = () => "123"; // 不会警告
let fn3: VoidFn = function () {
// 不会警告
return false;
};

// 且需要注意的是, 即使你能够让限制返回类型为void的返回值变成其他返回值,
// 并且该返回值也是可视的,但是你依旧无法使用其返回的内容进行任何操作,因为ts还是会将其保留为void类型

if (fn3()) {
// ❌,ts警告,无法测试 "void" 类型的表达式的真实性
console.log("fn3 run");
}

为什么会这样?参考官方文档

  • 官网说法是让以下的代码能够成立且有效:
let arr: number[] = [1, 2, 3, 4, 5];
let cache: number[];

// 箭头函数在省略大括号时,默认将箭头后的执行内容当作返回值
// 这里箭头函数后的push将会返回push后的数组长度,类型为number
arr.forEach((n) => cache.push(n));
// 针对 forEach 我们还可以这么写
arr.forEach((n) => {
cache.push(n);
});

// 因为TS 在针对一些属性方法的设计使用时,无法具体的去限制其行为所导致的结果,所以在针对该特点,放宽部分限制
// 诸如此类的属性方法还有许多,例如map、filter、reduce等

interface

它是定义结构的一种方式,主要作用是为类、对象、函数等规定一种契约,有效确保代码的一致性和类型安全。
interface只能定义格式,不能包含任何实现。

定义类结构
interface PostsInterface {
title: string;
date: number;
edit(s: string): void;
}
// 定义一个Post类,实现PostsInterface接口
class Post implements PostsInterface {
constructor(public title: string, public date: number) {}
edit(s: string) {
console.log(`新内容为:`, s);
}
}

// 新建一个文章
const post = new Post();
定义对象结构
interface PostInterface {
title: string;
subtitle?: string;
readonly data: number;
edit: (s: string) => void;
}

const post: PostInterface = {
title: "文章标题",
data: Date.now(),
edit(s: string) {
console.log(`新内容为:`, s);
},
};
定义函数结构
interface FnInterface {
(x: number, y: number): number;
}

const computed: FnInterface = (x, y) => {
return x * y;
};
接口之间的继承
interface PersonInterface {
name: string;
age: number;
sex: string;
}

interface DeveloperInterface extends PersonInterface {
skills: string[];
}

const develop: DeveloperInterface = {
name: "张三",
age: 18,
sex: "男",
skills: ["js", "css", "html"],
};
接口自动合并
interface PersonInterface {
name: string;
age: number;
sex: string;
}
interface PersonInterface {
height: number;
weight: number;
}
// ===
interface PersonInterface {
name: string;
age: number;
sex: string;
height: number;
weight: number;
}

接口的使用场景:

  • 定义对象格式:描述数据结构,配置对象等
  • 类的契约规范:规定一个类具体的属性和方法
  • 自动合并:用于扩展第三方库的类型

typeinterface的区别

  • 相同点typeinterface都可以拿来定义对象结构,两种方式在不同场景皆可互换使用。
  • 不同点
- `type`:可以定义类型别名、联合类型、交叉类型,不支持`继承`和`自动合并`。
- `interface`:更专注于定义`对象`和`类`的结构,支持`继承`和`自动合并`。

泛型

泛型允许我们在定义函数、类或接口时,使用类型参数来表示未指定的类型,这些参数在具体使用时,才被指定具体的类型,泛型可以让同一段代码适配多种类型,同时保持类型的安全性。

  • 注:T只是一种规范名缩写而已(Type),表明区域位置的类型
function fn<T>(s: T): T {
console.log("s", s);
return s;
}

fn<string>("123");
fn<number>(123);

多种泛型

  • 支持多个泛型一起使用
function edit<C, T>(s: C, dosomething: T): C | T {
// 返回值为联合类型
console.log(`新内容为:`, s, dosomething);

return s || dosomething;
}
edit<string, number>("新内容sss", Date.now());

泛型接口

  • 拓展interface声明目标的类型,自由度更高,灵活性更强
interface MessageInterface<T> {
data: T;
code: number;
message: string;
}

let message: MessageInterface<string[]> = {
data: ["1", "2", "3"],
code: 200,
message: "success",
};

泛型类

  • 与接口用法差不多
// 基本用法
class Demo<T> {
constructor(public data: T) {}

getData(): T {
return this.data;
}
}

const demo = new Demo<string>("123");
console.log(demo.getData());

// 通过泛型指定内部类型
interface BaseInterface<T> {
data: T;
code: number;
message: string;
}

type Data = {
name: string;
age: number;
};

const data: BaseInterface<Data> = {
data: {
name: "张三",
age: 18,
},
code: 200,
message: "success",
};

类型声明文件

类型声明文件是 TS 中的一种特殊文件,通常以.d.ts作为文件扩展名。主要作用在于为现有的JavaScript代码提供类型信息,让 TS 能够在使用相关的JavaScript库时或者模块时能够得到类型检查和提示

  • 默认在 TS 模块中引入 Js 文件时,TS 将会警告提醒无法识别导入内容

  • demo

    • utils.js
    export const add = (x, y) => x + y;
    export const mul = (x, y) => x * y;
    • b.ts
    import { add, mul } from "./utils.js"; // TS警告提示,无法找到模块“./utils.js”的声明文件。xxx隐式拥有 "any" 类型。

    console.log(add(1, 2));
    console.log(mul(2, 3));
  • 创建具名的.d.ts类型文件(指定的文件名称,这里我的是utils.js

    • 创建 utils.d.ts
    declare const add = (a: number, b: number) => number;
    declare const mul = (a: number, b: number) => number;

    export { add, mul };
    • 创建完成后,需要关闭编译器重新打开让 TS 编译,就可以让你的 TS 重新识别你的模块类型

三斜线指令

  • 三斜线指令是包含单个 XML 标签的单行注释。 注释的内容会作为编译器指令使用
  • 三斜线指令仅可放在包含它的文件的最顶端。 一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。 如果它们出现在一个语句或声明之后,那么它们会被当做普通的单行注释,并且不具有特殊的涵义。
  • 三斜线引用告诉编译器在编译过程中要引入的额外的文件

/// <reference path="..." />

  • 语法:path指定文件名

    • 当使用--out--outFile时,它也可以做为调整输出内容顺序的一种方法。 文件在输出文件内容中的位置与经过预处理后的输入顺序一致。
    /// <reference path="xxx.d.ts" />
  • 预处理输入文件

    • 编译器会对输入文件进行预处理来解析所有三斜线引用指令。 在这个过程中,额外的文件会加到编译过程中。
    • 这个过程会以一些根文件开始; 它们是在命令行中指定的文件或是在 tsconfig.json 中的"files"列表里的文件。 这些根文件按指定的顺序进行预处理。 在一个文件被加入列表前,它包含的所有三斜线引用都要被处理,还有它们包含的目标。 三斜线引用以它们在文件里出现的顺序,使用深度优先的方式解析。
    • 一个三斜线引用路径相对于包含它的文件的,如果不是根文件
  • 错误

    引用不存在的文件会报错。 一个文件用三斜线指令引用自己会报错。

  • 使用 --noResolve

    如果指定了--noResolve编译选项,三斜线引用会被忽略;它们不会增加新文件,也不会改变给定文件的顺序

///<reference types="..." />

  • 这个指令是用来声明依赖的; 一个 /// <reference types="modulename" />指令则声明了对某个包依赖
  • 例如,把 /// <reference types="node" />引入到声明文件,表明这个文件使用了 @types/node/index.d.ts里面声明的名字; 并且,这个需要在编译阶段与声明文件一起被包含进来。
  • 注:仅当在你需要写一个d.ts文件时才使用这个指令。

///<reference no-default-lib="true" />

  • 这个指令把一个文件标记成默认库。 你会在 lib.d.ts文件和它不同的变体的顶端看到这个注释。
  • 这个指令告诉编译器在编译过程中不要包含这个默认库(比如,lib.d.ts)。 这与在命令行上使用 --noLib相似。
  • 注意,当传递了--skipDefaultLibCheck时,编译器只会忽略检查带有/// <reference no-default-lib="true"/>的文件。

///<amd-module />

  • 默认情况下生成的AMD模块都是匿名的。 但是,当一些工具需要处理生成的模块时会产生问题,比如 r.js。
  • amd-module指令允许给编译器传入一个可选的模块名
///<amd-module name='Demo'/>
export class Demo {}
  • 会将Demo传入到AMD define函数
define("Demo", ["require", "exports"], function (require, exports) {
var Demo = (function () {
function Demo() {}
return Demo;
})();
exports.Demo = Demo;
});

内容参考来自官方中文文档,英文在这:TypeScript Doc