不一样的 TypeScript 入门手册

lxf2023-05-06 00:56:02

前言

TypeScript是大势所趋,也是现在大厂必备技能,作为前端我们要与时俱进,此时不学,更待何时。

这篇文章可能不太适合 TS 纯小白,需要你对 TS 有一定的了解,这样的话,食用起来味道更佳。阅读的过程中一定要有耐心,不要急于求成,建议认真看完每一个字并且边学边敲,这样才能加深印象,不至于睡一觉就忘,浪费大把时间。从JavaScript过渡到TypeScript其实很简单,因为两者语法类似,学习成本并不高,掌握这篇文章中的内容足够日常使用。

社区里有不少关于TypeScript的文章,热门的我基本都看过,大佬们写的也确实很不错,膜拜!但是我觉得还可以站在巨人的肩膀上再完善一下,内容上对于新手可以再友好一些,篇幅上也可以再精简一点。另外我想通过写作的方式进一步巩固自己的 TS 知识,学而时习之,不亦说乎。

怀着这样的初衷,我开始动笔,如果这篇文章能帮助到你,那是我莫大的荣幸;如果你在阅读过程中发现错误或者不足之处,欢迎指正,我们共同进步。

什么是 TypeScript

想学好一门语言,我们首先要搞清楚它是什么。

TypeScript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成。TypeScript可以通过TypeScript编辑器Babel 转译为 JavaScript 代码,可以运行在任何浏览器,任何操作系统。

TypeScript 起源于使用JavaScript开发的大型项目 。由于JavaScript语言本身的局限性,难以胜任和维护大型项目开发,因此微软开发了TypeScript ,使得其能够胜任开发大型项目。

这些概念不用死记硬背,了解即可。

简单总结:TypeScriptJavaScript的超集,具有类型系统并可以编译为纯JavaScript

为什么要使用 TypeScript

任何一门语言的诞生和发展都是有缘由的,从某种程度上说,TypeScript的诞生是历史发展的必然。

Web 应用越来越复杂,导致JavaScript代码量激增,由于JavaScript是动态语言,很难做到类型检查,这直接导致很多语法问题在编译阶段无法被发现,只能在运行时暴露。(想想都头大)

TypeScript是静态类型的语言,静态类型可以让编译器在编码阶段即时检测到各类语法错误。使用TypeScript进行开发,能够避免许多潜在的 bug 。

通过是否允许隐式转换来分类
强类型:TS
弱类型:JS

通过类型检查的时机来分类
静态类型:TS
动态类型:JS

TypeScript给前端带来的好处主要有以下几点:

  • 提高开发效率和代码质量

    TypeScript不仅可以让编辑器进行智能提示和语法错误检查,还能够实现代码补全、接口提示、跳转到定义和代码重构等操作。现在你可能无法理解,等真正上手用起来,真香!

  • 增强了代码的可读性和可维护性

    一般来说,理解 C# 或者 Java 会比 JavaScript 更容易,因为他们都是强类型的,而且支持面向对象的特征。强类型语言本身就是一个很好的说明文档,大部分函数看类型定义就能明白大致如何使用。JavaScript很多库中利用了不少高级语言的特性,开发人员可能无法很好地理解其意图,而TypeScript可以协助我们解决这样的问题。

  • 胜任大规模应用开发

    使用TypeScript开发的项目,代码结构更加清晰、一致和简单,降低了代码后续维护和升级的难度。 也有小部分人质疑TypeScript,认为没必要去学习。在我看来,这不过是给自己的懒惰寻找借口,当大潮退去,才知道谁在裸泳。

搭建学习环境

强烈推荐一个官方的云编辑器 Playground

使用 Playground 就无需在本地安装环境,通过浏览器就可以随时学习TypeScript,综合体验也不比本地编辑器差,很适合新手使用。

TypeScript初体验

const a: string = 1; // Type 'number' is not assignable to type 'string'

上面这行代码与普通 JS 代码的区别是,在变量后面加了一个:string,这代表只能给变量astring类型的值。我们将一个number类型的值赋值给变量 a,所以报错:number 类型不可分配给 string 类型。

在 TS 中,这叫做类型注解类型注解是一种为函数或者变量添加约束的方式。

基本数据类型

八种内置类型

跟 JS 的八种内置类型一致:

const str: string = '中国万岁';
const num: number = '666';
const bool: boolean = true;
const u: undefined = undefined;
const n: null = null;
const big: bigint = 100n;
const sym: symbol = Symbol('me');
const obj: object = {x: 1};

动手敲一敲,很容易理解。

注意:

null 和 undefined

默认情况下nullundefined是所有类型的子类型,可以把nullundefined赋值给其它任何类型:

// null 和 undefined 赋值给 number
let num: number = 1;
num = null;
num = undefined;
 
// null 和 undefined 赋值给 boolean
let bool: boolean = false;
bool = null;
bool = undefined;

// null 和 undefined 赋值给 object
let obj: object = {};
obj = null;
obj = undefined;

如果在tsconfig.json配置"strictNullChecks": truenull就只能赋值给anyunknown和它本身的类型(null),undefined就只能赋值给anyunknownvoid和它本身的类型(undefined)。

number 和 bigint

虽然numberbigint都表示数字,但是这两个类型并不兼容:

let big: bigint = 100n;
let num: number = 1;
num = big; // Type 'bigint' is not assignable to type 'number'

其它类型

Array

定义数组的类型有两种方式:

1. let arr: string[] = ['剑圣', '蛮王'];
2. let array: Array<string> = ['剑姬', '锐雯'];

这两种写法都意味着,数组里面的值只能是 string 类型,否则就会报错:

arr.push(8); // Argument of type 'number' is not assignable to parameter of type 'string'
array = ['剑姬', '锐雯', 6]; // Type 'number' is not assignable to type 'string'

推荐使用第一种写法。第二种是泛型写法,现在你不需要掌握,后面会讲到。

如果你不仅想在数组中存储 number 类型的值,还想存储 string 类型的值,可以这样写:

// 这叫联合类型数组,先了解一下。
let arr: (number | string)[] = [1, '1'];

元组

什么是元组

元组是 TS 特有的类型,跟数组类似。元组最重要的特征是可以限制数组元素的个数和类型,看栗子:

// [string, number] 就是元组类型。数组 x 的类型必须严格匹配,且个数必须为2
let x: [string, number]; 

x = ['Hi', 666]; // OK 
x = [666, 'Hi']; // error
x = ['Hi', 666, 888]; // error

注意: 元组只能表示一个已知元素数量和类型的数组,越界就会报错。如果一个数组中可能有多种类型,且数量也不确定,那就直接使用 any[]。any 大家应该都不陌生吧,anyScript,YYDS !

元组类型的解构赋值

元组同样支持解构赋值:

let arr: [string, number] = ['德玛西亚!', 666];
let [lol, action] = arr;
console.log(lol); // 德玛西亚!
console.log(action); // 666

当元组中的元素较多时,这种方式就不可取了。另外需要注意,解构数组元素的个数是不能超过元组中元素个数的:

let arr: [string, number] = ['德玛西亚!', 666];
let [lol, action, hero] = arr; // Tuple type '[string, number]' of length '2' has no element at index '2'

元组类型[string, number]的长度是 2,在位置索引 2 处没有任何元素。

元组类型的可选元素

在定义元组类型时,我们也可以通过?来声明元组类型的可选元素:

// 要求包含一个必须的字符串属性,和一个可选的布尔值属性
let arr: [string, boolean?];

arr = ['一个能打的都没有', true];
console.log(arr); // ['一个能打的都没有', true]

arr = ['如果暴力不是为了杀戮'];
console.log(arr); // ['如果暴力不是为了杀戮']

元组类型的剩余元素

元组类型里最后一个元素可以是剩余元素,形式为...x,你可以把它当作 ES6 中的剩余参数。剩余元素代表元组类型是开放的,可以有0个或者多个额外的元素。例如,[number, ...string[]]表示带有一个number类型的元素和任意数量string类型的元素的元组类型。举个栗子:

let arr: [number, ...string[]];
arr = [1, '赵信']; // ok
arr = [1, '赵信', '吕布', '亚索']; // ok

只读的元组类型

我们可以为任何元组类型加上readonly关键字前缀,使其成为只读元组:

const arr: readonly [string, number] = ['断剑重铸之日', 666];

在使用readonly关键字修饰元组类型后,任何企图改变元组中元素的操作都会报错:

// Cannot assign to '0' because it is a read-only property
arr[0] = '骑士归来之时';

// Property 'push' does not exist on type 'readonly [number, string]'
arr.push(6);

函数

函数声明

function sum(x: number, y: number): number {
    return x + y;
}

上面这段代码表示,sum函数接收两个number类型的参数,并且它的返回值也是number类型。

函数表达式

const sum = function (x: number, y: number): number {
    return x + y;
}

箭头函数

const sum = (x: number, y: number): number => x + y; 

可选参数

function queryUserInfo(name: string, age?: number) {
    if (age) {
        return `我叫${name},${age}岁`;
    }
    return `我叫${name},年龄保密`;
}

queryUserInfo('王思聪', 18); // 我叫王思聪,18岁(有钱人永远18岁!)
queryUserInfo('孙一宁'); // 我叫孙一宁,年龄保密

注意: 可选参数后面不允许再出现必需参数:

// 报错:A required parameter cannot follow an optional parameter
function queryUserInfo(name: string, age?: number, sex: string) {
    ...
}

参数默认值

可以给参数一个默认值,当调用者没有传该参数或者传入了undefined时,这个默认值就生效了。

function queryUserInfo(name: string, age: number, sex: string = '不详') {
    return `姓名:${name},年龄:${age},性别:${sex}`; 
}

queryUserInfo('xxx', 26); // 姓名:xxx,年龄:26,性别:不详

注意: 有默认值的参数也可放置在必需参数的前面,如果想要触发这个参数的默认值,必须要主动的传入undefined才可以。

剩余参数

function push(arr: any[], ...items: any[]) {
    items.forEach(item => arr.push(item));
}

let array: any[] = [];
push(array, 1, 2, 3, '迪丽热巴', '古力娜扎');
console.log(array); // [1, 2, 3, '迪丽热巴', '古力娜扎']

函数重载

由于 JS 是动态类型语言,我们经常会使用不同类型的参数来调用同一个函数,该函数会根据不同的参数返回不同类型的调用结果:

function sum(x, y) {
    return x + y;
}

sum(1, 2); // 3
sum('1', '2'); // 12 (string)

以上代码可以在TS中直接使用,但是如果开启noImplicitAny配置项,那么就会提示错误信息:

Parameter 'x' implicitly has an 'any' type
Parameter 'y' implicitly has an 'any' type

该提示信息告诉我们:参数 x 和参数 y 隐式具有any类型。为了解决这个问题,就要给参数定义类型。

此时我们希望sum函数的入参可以同时支持stringnumber类型,所以我们可以先定义一个联合类型string | number,再给这个联合类型取个名字:

type UnionType = string | number;

这叫做类型别名,先了解一下,也不难理解~

接下来我们重写一下sum函数:

function sum(x: UnionType, y: UnionType) {
    if (typeof x === 'string' || typeof y === 'string') {
        return x.toString() + y.toString();
    }
    return x + y;
}

sum函数的参数显示地设置类型之后,错误提示就消失了。下面我们验证一下:

const res = sum('你', '好');
res.split('');

一切看起来似乎很正常,我们想当然的认为res变量的类型为string,所以我们可以正常调用字符串方法split。但此时 TS 编译器却报错了:

Property 'split' does not exist on type 'string | number'
Property 'split' does not exist on type 'number'

类型number上不存在split属性。该如何解决?函数重载闪亮登场。

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力,要解决上面的问题,就要为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。看栗子:

type UnionType = number | string;
function sum(x: number, y: number): number;
function sum(x: string, y: string): string;
function sum(x: string, y: number): string;
function sum(x: number, y: string): string;
function sum(x: UnionType, y: UnionType) {
    if (typeof x === 'string' || typeof y === 'string') {
        return x.toString() + y.toString();
    }
    return x + y;
}

const res = sum('你', '好');
res.split('');

上面的栗子中,我们为sum函数提供了各种情况的函数类型定义,从而实现函数的重载,解决了报错问题。此处强烈建议大家动手敲一遍,根据不同函数类型定义进行验证,加深印象。

any

在 TS 中,任何类型都可以被归为any类型,any类型是类型系统的顶级类型。

如果是一个普通类型,在赋值过程中改变类型是不被允许的:

let a: string = '伊泽瑞尔,你需要地图吗?';
a = 666; // Type 'number' is not assignable to type 'string'

但如果是any类型,则允许被赋值为任意类型:

let a: any = 666;
a = '哈哈哈';
a = false;
a = null;
a = undfined;
a = [];
a = {};

如果变量在声明的时候,未指定其类型,那么它会被识别为any类型:

let something;
something = '啦啦啦';
something = 888;
something = false;

等价于:

let something: any;
something = '啦啦啦';
something = 888;
something = false;

使用any类型就失去了使用TS的意义,长此以往会放松我们对自己的要求,尽量不要使用any

unknown

unknownany十分相似,所有类型都可以分配给unknown类型:

let a: unknown = 250;
a = '面对疾风吧!';
a = true;

unknownany最大的区别是:任何类型的值都可以赋值给any,同时any类型的值也可以赋值给任何类型(never除外)。任何类型的值都可以赋值给unknown,但unknown类型的值只能赋值给unknownany

let a: unknown = 520;
let b: any = a; // ok

let a: any = 520;
let b: unknown = a // ok

let a: unknown = 520;
let b: number = a; // error

如果不缩小类型,就无法对unknown类型执行任何操作:

function battle() {
    return 'victory !'
}

const record: unknown = {hero: battle};
record.hero(); // error

这种机制起到了很强的预防性,更安全。

我们可以使用typeof或者类型断言等方式来缩小未知范围:

const a: unknown = '超神!';
a.split(''); // error

if (typeof a === 'string') {
    a.split(''); // ok
}

// 类型断言,后面会讲到
(a as string).split(''); // ok

void

void表示没有任何类型,和其它类型是平等关系,不能直接赋值:

let a: void;
let b: number = a; // Type 'void' is not assignable to type 'number'

声明一个void类型的变量没有什么意义,一般只有在函数没有返回值时才会使用到它。

never

never类型表示的是那些永不存在的值的类型。

值会永不存在的两种情况:

  1. 如果一个函数执行时抛出了异常,那么这个函数就永远不存在返回值;
  2. 函数中执行无限循环的代码,也就是死循环。
// 抛出异常
function error(msg: string): never { // ok
    throw new Error(msg);
}

// 死循环
function loopForever(): never { // ok
    while (true) {}
}

never类型同 nullundefined一样,也是任何类型的子类型,也可以赋值给任何类型。

但是没有类型是never的子类型或可以赋值给never类型(除了never本身之外),即使any也不可以赋值给never

let a: never;
let b: never;
let c: any;

a = 250; // error
a = b; // ok
a = c; // error

在 TS 中,可以利用never类型的特性来实现全面性检查,看栗子:

type Type = string | number;

function inspectWithNever(param: Type) {
    if (typeof param === 'string') {
        // 在这里收窄为 string 类型
    } else if (typeof param === 'number') {
        // 在这里收窄为 number 类型
    } else {
        // 在这里是 never 类型
        const check: never = param;
    }
}

在 else 分支里,我们把既不是string类型也不是number类型的param赋值给了一个显式声明的never类型的变量,如果一切逻辑正确,那么就可以编译通过。假如有一天你的同事修改了Type的类型:

type Type = string | number | boolean;

然而他忘记了同时修改inspectWithNever方法中的控制流程,这时else分支的param类型会被收窄为boolean类型,导致无法赋值给never类型,此时就会出现一个错误提示。

通过这种方法,我们可以确保inspectWithNever方法总是穷尽了Type的所有可能类型,使得代码的类型绝对安全。

object、Object、{}

  • object:以下称小object
  • Object:以下称大Object
  • {}:以下称空对象 小object代表的是所有非原始类型,也就是说我们不能把number string等原始类型赋值给小object。在严格模式下,nullundefined类型也不能赋值给小object
以下类型被视为原始类型:stringnumberbooleannullundefined、bigInt、symbol

看栗子:

let obj: object;

obj = 1; // error
obj = '人在塔在!'; // error
obj = true; // error
obj = null; // error
obj = undefined; // error
obj = 100n; // error
obj = Symbol(); // error
obj = {}; // ok

大Object代表所有拥有toString hasOwnProperty方法的类型,所以,所有原始类型和非原始类型都可以赋值给大Object。同样,在严格模式下nullundefined类型也不能赋给大Object

let obj: Object;

obj = 1; // ok
obj = '人在塔在!'; // ok
obj = true; // ok
obj = null; // error
obj = undefined; // error
obj = 100n; // ok
obj = Symbol(); // ok
obj = {}; // ok

从上面的栗子中可以看出,大Object包含原始类型,而小object仅包含非原始类型。你可能会想,那么大Object是不是小object的父类型?实际上,大Object不仅是小object的父类型,同时也是小object的子类型。为了证明这一点,我们举个