来吧,一起对TypeScript扫盲吧!🐬

61 阅读11分钟

对于前端小伙伴来说,TypeScript肯定都不陌生,但本人之前一直对TS了解的不多,这次学习一下TS并总结成文章。

废话不多说,直接开始👊

TypeScript 概览

  1. TypeScript 是什么?

简单理解就是TypeScript是增加了类型约束的JavaScript,并且可以被编译成原生JavaScript。

  1. 为什么需要TypeScript?

a. 与弱类型的JS结合,在编译期间增强类型检查,提前发现可能的缺陷。

b. 通过强类型约束可以放心地进行多人协作开发,保证项目的可维护性。

c. 与代码编辑器集成,提供自动补全、引用跳转等实用功能,提升开发效率。

基本用法

下面来看看TypeScript的基本用法

基本类型

简单类型介绍

对于简单类型呢,就是string、number、boolean、symbol、undefined和null,比较基础:

const str:string = 'hello';
const num:number = 1;
const isAfternoon:boolean = true;
let result:undefined = undefined;
let variable:null = null;
自动推断类型

在某些场景,TS是可以自己推断出类型的,比如:

  • 初始化赋值的时候
let myName = 'Daniel Yang';
myName = 123; // 让我们看看将数字类型赋值给 myName 会发生什么?

duang~ ts发报告报错:

WechatIMG271.jpeg

  • 对函数的返回值
function greet(name:string){
    return `Hi,My name is ${name}.`;
}

TS会自动推断出返回值类型:

WechatIMG272.jpeg

  • 存在比较明显的上下文推断
const arr = [1,2,3];

在map方法中ts能推断出遍历元素的类型:

WechatIMG273.jpeg

在这些场景下由于ts能推断出具体类型,所以是可以省略类型注释的,还能 减少代码的长度。

特别的类型

下面介绍一些特别的类型

1. any

在ts里有一个很特殊的any类型,对于不知道具体类型 或者就是不想写类型的情况,可以使用 any 来声明。

不过这样会导致ts对该变量禁用检查,丢失掉ts该有的作用,所以需要避免过度使用 any。

2. unknown

unknown代表着任意的值,它和 any非常像,但由于对unknown进行任意操作都是不合法的,所以它比直接使用 any 更安全。

function fnWithAny(a:any){
    a.b(); // it's OK.
}
function fnWithUnknown(a:unknown){
    a.b(); // error:a is of type 'unknown'.
}

3. never

never意味着永远不会发生,对于抛出异常会提前终止执行的函数来说,适合对其返回类型声明为never:

function fail():never{
    throw new Error('oops')
}

看起来好像没啥用

但其实 never 非常适合用于防止对联合类型有遗漏使用的情况,例如:

type Shape = 'circle' | 'square';

let shape:Shape;

switch(shape){
    case 'circle':
    // some logic
    break;
    case 'square':
    // some logic
    break;
    default: //按照正常逻辑是走不到default分支的
        const val:never = shape; //此时 shape 为never类型
        break;
}

有意思的地方来了:

如果有一天大家对Shape增加了新类型star,但是忘记了去新增switch的case分支,此时 default分支里ts会报错导致代码编译不通过,将这个遗漏case分支的隐患暴露出来!

WechatIMG274.jpeg

4. void

void意味着函数没有返回值或不返回任何明确的值:

function noop1():void{
    console.log('noop')
}

function noop2():void{
    console.log('Just nothing.');
    return;
}

复杂类型

接下来我们来看下如何在ts里给复杂对象添加类型声明

首先来认识一下 type 和 interface 关键字

1. type类型别名

在ts里,我们可以使用type关键词来给任意类型添加命名,这样可以方便引用和复用:

// 添加 Point 的类型别名
type Point = {
    x: number;
    y: number;
}
function printCoord(pt: Point){
    console.log("coordinate's x and y is: ", pt.x,pt.y);
}

同时我们可以使用 & 符号将多个type进行组合:

type Animal = {
    name: string;
    eat: () => void;
}

type DogAction = {
    bark: () => void;
    walk: () => void;
}

type Dog = Animal & DogAction; // 组合
let dog: Dog;
dog.walk();

2. interface接口类型

interface是另一种用来声明对象类型的方式:

interface Point{
    x: number;
    y: number;
}

function printCoord(pt: Point){
    console.log("coordinate's x and y is: ", pt.x,pt.y);
}

我们可以使用extends关键字对interface进行继承:

interface Animal{
    name:string;
    eat: () => void;
}
interface Dog extends Animal { //继承
    bark: () => void;
    walk: () ==> void;
}
let dog:Dog;
dog.walk();

既然有两种类型声明的方式,那么问题来了, type 和 interface 有啥区别呢?🤔

type 和 interface 的区别

type 和 interface 主要有以下几个区别:

  1. interface只能声明对象类型,但type除了对象类型以外,还可以声明简单类型和union联合类型。
// 对象类型
interface Info{
    name: string;
    desc: string;
}
type Info = {
    name: string;
    desc: string;
}
// type 还可以声明简单类型和联合类型
type name = string;
type value = string | number;
  1. interface的重复声明可以合并,然而type不能重复声明:
// interface 可以重复声明,声明的属性会进行合并
interface Info {
    name: string;
}
interface Info {
    desc: string;
}
type Info = {
    name:string;
}
type Info = { // Error:type 不能重复声明
    desc:string;
}
  1. type 和 interface实现类型扩展的方式不同

type通过&符号进行类型合并,而interface通过extends关键词实现继承

interface A {
    a: string;
}
interface B extends A {
    b: number;
}
// interface B => { a:string;b:number; }

type A = {
    a: string;
}
type B = A & {
    b: number;
}
// type B => { a:string;b:number; }

3. 对象

讲完了类型声明的方式,我们来看看在ts里如何对对象进行类型声明,如下所示:

interface Info {  
    name: string;  
    desc: string;  
}

同时我们可以用 ? 和readonly 修饰符来修饰对象属性:

  • ? 是可选修饰符,意味着该属性可以不赋值
type Info = {
    name:string;
    phone?:string; // phone => string | undefined
}
  • readonly 是只读修饰符,表示该属性初始化后不能再次修改
type Indeo = {
    readonly name:string;
}
let info:Info = {
    name:'Daniel'
}
info.name = 'Tom'; // Error:Cannot assign to 'name' because it is a read-only property.

在使用可选属性前需要检查属性是否存在,否则ts会产生报错提示:

function printName(obj:{ first:string,last?:string }){
    console.log(obj.lase.toUpperCase()); // Error:'obj.last' is possibly 'undefined'.
    
    if(obj.last !== undefined) {
        console.log(obj.last.toUpperCase()); // OK.
    }
    console.log(obj.last?.toUpperCase()); //或者使用JS的?.语法糖
}

对于readonly来说虽然不会真的改变属性的性质,但会在编译期的类型检查期间禁止属性的重新写入:

function doSomething(obj: {readonly message:string }){
    obj.message = 'hello'; // Error:Cannot assign to 'message' because it is a read-only property.
}

readonly修饰符与const声明类似的,它并不意味着属性的值完全不能修改,而是指不能再重新更新属性的引用:

type PersonalInfo = {
    readonly baseInfo: { // baseInfo 是一个对象
        name:string;
        gender:string;
        age:number;
    }
}
function getPersonalInfo(person:PersonalInfo) {
    person.baseInfo.age ++; //可以更新它的属性值
    
    person.baseInfo = { // 但不能更新它的引用
        name: 'Yang',
        // ...
    }
}

4. 数组

对数组来说,它的类型声明有两种方式,以字符串数组为例:

  • string[]
  • Array< string>

这两种写法的结果没有区别,只是第二种是泛型U< T>的写法,我们稍后再详细介绍泛型

与对象属性一样,我们也可以将数组声明为只读数组,同样有两种方式:

  • ReadonlyArray< string>
  • readonly string[]

这样使得数组内容不可更改:

const arr: readonly string[] = ['apple','banana'];

arr[2] = 'orange'; //Error:Index signature in type 'readonly string[]' only permits reading.
arr1.push('orange');//Error:Property 'push' does not exist on type 'readonly string[]'

5. 函数

对函数来说,需要声明类型的地方有 函数参数 和 函数返回值,例如:

function getmax(a: number,b: number): string {
    return `The max is ${ Math.max(a,b)}`;
}

同样地,我们也可以声明可选参数和只读参数:

function fixed(n:number,digit?: number) {
    if(digit !== undefined) {
        return n.toFixed(digit);
    }
    return n.toFixed();
}
function fn(obj: {readonly a: number }){
    console.log('obj.a is:',obj.a);
    obj.a ++; // Error:Cannot assign to 'a' because it is a read-only property.
}

常用类型

接下来介绍几种常用的类型

1. union 联合类型

联合类型是将两个以上的类型组合起来的形式,表示某个值可以是其中任意一个类型:

function printId(id: number | string){
    console.log('Your ID is:',id);
}
printId(101); //OK
printId('202'); //OK
printId({ id:303 }); // Error:Argument of type '{ id:number; }'is not asssignable to parameter of type 'string | number'.

除了类型联合外,我们还可以联合具体的值,这样在代码编辑器里还能方便地增加提示:

function printText(s: string,alignment:'left' | 'right' | 'center'){}

WechatIMG275.jpeg

需要注意的是:在TS里使用联合类型时,只有当某个属性是所有类型所共有的才可以直接用

比如某个联合类型是string | number,如果直接使用只存在于string类型上的属性和方法是只会喜提报错的:

function print(val: string | number){
    if(typeof val.split === 'function'){
    //Error:Property 'split' does not exist on type 'number'.
        console.log(val.split(''));
    }
}

就是说在使用某一类型特有的属性之前,需要通过明确的类型判断让TS知道变量具体的类型,这样就能正常使用类型所对应的属性和方法了。

总结了下 => 至少有以下几种方式可以用来更明确地判断变量的类型:

  • 使用 typeof 操作符
function padLeft(padding: number | string,input: string){
    if(typeof padding === 'number'){ // 使用typeof明确变量的类型
        return ''.repeat(padding) + input;
    }
    return `${padding}${input}`;
}
  • 使用 in 操作符
type Fish = { swim:() => void };
type Bird = {fly:() => void };

function move(animal:Fish | Bird){
    if('swim' in animal){
        //检查swim是否存在于animal原型链上,即是否为Fish类型
        animal.swim();
    }else{
        animal.fly();
    }
}
  • 使用instanceof操作符
function logValue(x: Date | string){
    if(x instanceof Date){ // 是否为 Date类型实例
        console.log(x.toUTCString());
    }else {
        console.log(x);
    }
}
  • 使用自定义类型预测方法

除了使用JS本身的语言能力来做,我们也可以自定义一些类型判断方法

比如我们需要判断一个变量究竟是Fish类型还是Bird类型,可以这样写:

function isFish(pet:Fish | Bird):pet is Fish {
    return (pet as Fish).swim !== undefined;
    //验证下pet变量上是否存在swim属性
}

然后放在条件判断里就好了:

if(isFish(pet)){
    pet.swim();
}else{
    pet.fly();
}

2. enum枚举

enum枚举是TS在JS语法之外新增的特性,它允许我们定义一组命名常量,比如:

enum NumericDirection {
    Up, // 默认从0开始,后面的变量如果没有赋值则继续加1
    Down, // 1
    Left, // 2
    Right, // 3
}
enum StringDirection {
    UP = 'UP',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right',
}

简单来说就是:

  • 数字类型的枚举默认值为0,后面的成员如果没有赋值则继续累加1
  • 字符类型的枚举必须要赋值

枚举成员也可以是混合类型,例如这样:

enum MixedType {
    A, // 0
    B:'B'
}

比较有意思的是枚举其实是真实的对象,所以在代码里可以作为值直接使用:

enum Response {
    No, // 0
    YES, // 1
}
// 作为类型的值
function handleResponse(type:Response,message:string){}
handleResponse(Response.YES,'success')

// 传递给函数参数
function fn(n: number) {
    if(n === Response.YES){}
}
fn(Response.NO);

如果是这样,那问题又来了:TS的枚举和JS的对象有什么区别呢?

枚举和对象主要有两点不同:

  • 数字类型的枚举会生成反向映射,可以通过枚举的值获取到对应的健key:
enum NumericEnum {
    LEFT = 1,
    RIGHT = 2,
}
NumericEnum[NumericEnum.LEFT]; // 'LEFT'
NumericEnum[1]; // 'LEFT'

// 让我们打印下NumericEnum的key
for(const key of Object.keys(NumericEnum)) console.log(key)

// '1'
// '2'
// 'LEFT'
// 'RIGHT'
// 不是很明白为什么要这样设计
  • 枚举成员是只读类型
NumericEnum['LEFT'] = 3; // Error: Cannot assign to 'LEFT' because it is a read-only property.

3. Tuple元组

介绍完枚举,我们来认识下Tuple元组

名字听起来很高大上,但其实,它就是数组而已。

不过在元组里可以混合着不同类型,比如pair:[string,number],它就属于元组。

由于元组一般是知道元素数量和对应的类型,所以TS可以对元组的下标访问是否越界和具体元素的操作是否合法做检查:

function doSomething(pair:[string,number]){
    console.log('first value is:',pair[0]); // It's OK.
    console.log('third value is:',pair[2]); // Error:Tuple type '[string,number]'
    console.log(pair[1].split('-')); // Error:Property 'split' does not exist on type 'number'.
}

为什么说是一般呢,因为元组里可以有可选元素和扩展元素,它们会造成元组的实际长度不确定

  • 可选元素:我们可以在元素类型后面增加?表示其为可选元素,需要注意可选元素只能出现在队尾
type TupleArray = [number,string,boolean?];
const arr1:TupleArray = [1,'2']; // OK.
const arr2:TupleArray = [1,'2',true]; // OK.
  • 扩展元素:和JS语法一样,我们可以用在类型前添加...表示它是一个扩展元素:
// 表示前两个元素分别是字符和数字类型,剩下的元素都是布尔类型
type StringNumberBooleans = [string,number,...boolean[]];
// 表示第一个和最后一个元素分别是字符和数字类型,中间的元素都是布尔类型
type StringBooleansNumber = [string,...boolean[],number];
// 表示最后两个元素分别是字符和数字类型,前面的元素都是布尔类型
type BooleansStringNumber = [...boolean[],string,number];

进阶用法

恭喜你,能看到这里的人都是大佬,下面我们来学一些TS的进阶用法😎

函数

函数重载

如果某个函数能够以不同的参数数量和参数类型来调用,那在TS里该如何对该函数进行类型声明呢?

答案是:我们可以定义多个函数签名

比如我们要写一个展示日期的方法,该方法可以接收一个 数字类型的时间戳参数 或具体年月日三个参数,那么可以这样写函数的类型声明:

// 函数签名
function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;

// 函数实现
function makeDate(yOrTimestamp: number, month?: number, day?: number): Date {
    if (month !== undefined && day !== undefined) {
        return new Date(yOrTimestamp, month, day);
    }
    return new Date(yOrTimestamp);
}

// 函数调用
const d1 = makeDate(123456789); // ✅ OK.
const d2 = makeDate(2023, 7, 30); // ✅ OK.
const d3 = makeDate(2016, 10); // ❌ Error: No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.

不过需要强调的是:如果能用union联合类型声明的,就不要用重载来声明,否则会把简单问题复杂化。

比如我们需要写一个返回字符串或数组长度的方法,假设使用重载来进行类型声明:

// 函数签名  
function len(s: string): number;  
function len(arr: any[]): number;  
  
// 函数实现  
function len(x: any) {  
    return x.length;  
}

在普通调用下没有问题:

len('hello'); // ✅ OK.  
len([123]); // ✅ OK.

但如果像下面这样调用,TS就会报错:

len(Math.random() > 0.5 ? 'hello' : [123]); // ❌ Error:

因为此时参数类型在编译时没法确定,不能单独匹配任意一个函数签名:

WechatIMG276.jpeg

但冷静下来想一想,在这种参数数量和返回值类型都相同的情况下,直接使用union联合类型不香吗:

function len(x: string | any[]): number {  
    return x.length;  
}  
  
len(Math.random() > 0.5 ? 'hello' : [123]); // ✅ OK.

完美解决!

函数泛型

下面我们来了解下TS里一个比较重要的概念:泛型

泛型是用来描述同一类型在多个值之间的关联性

比如某个方法需要返回数组参数的第一元素,虽然可以像这样写类型声明:

function getFirstElement(arr: any[]) {  
    return arr[0];  
}

但这样会导致方法的返回值是 any 类型,优点简单粗暴,表达不了返回值和入参的关系

WechatIMG277.jpeg

如果返回值的类型能明确地与入参数组的元素类型关联上就好了

此时我们就可以使用泛型来满足这个需求,如下:

function getFirstElement<Type>(arrType[]): Type {  
    return arr[0];  
}

通过在函数签名处添加一个类型参数 Type 并用在参数列表和返回值声明里,我们就在它们俩之间建立了联系。

现在当我们调用函数时,返回值的类型将会与数组元素的类型一致:

WechatIMG278.jpeg

WechatIMG279.jpeg

同时我们还可以使用extends关键字 对泛型增加限制

比如我们需要实现一个 在两元素中返回length属性最大的那个元素 方法:

function getLonger<Type extends { length: number }>(aTypebType): Type {  
    if (a.length > b.length) {  
        return a;  
    }  
    return b;  
}

这样就限制了该泛型必须具有number类型的length属性:

getLonger(1020); // ❌ Error: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.  
getLonger([10], [20]); // ✅ OK.

对象

索引签名 index signature

在实际项目中会存在这样一种情况:我们不知道一个类型里所有的属性值,但巧的是我们知道属性key 和对应值的类型。

此时就可以用索引签名来进行类型晟敏

比如可以这样声明一个下标是数字、值是字符串的对象:

interface StringArray {  
    [index: number]: string;  
}  
  
const myArrayStringArray = getStringArray();  
myArrsy[0]; // type: string;

But 只有string、number 和 symbol可以用作对象key的类型,这也符合JS语言中对象key类型的范围。

如果对象的属性有不同类型,我们可以用union联合类型来声明值的类型:

interface NumberOrStringDic {  
    [key: string]: number | string;  
    length: number;  
    name: string; // ✅ It's OK.  
}

最后,我们也可以给索引签名增加readonly前缀来防止属性被重新赋值:

interface ReadonlyStringArray {  
    readonly [index: number]: string;  
}  
  
const myArrayReadonlyStringArray = getReadonlyStringArray();  
myArray[0] = 'Daniel'// ❌ Error: Index signature in type 'ReadonlyStringArray' only permits reading.
对象泛型

与函数一样,对象也存在泛型声明

假设有这样一个对象Box,它有一个包含任意类型的content属性,讲道理我们可以这样声明:

interface Box {  
    content: any;  
}

这没有问题,但使用 any 会导致TS对 content 属性移除了类型检查,比如:

const boxBox = {  
    content'string type'  
}  
  
box.content(); // 字符串不能直接作为方法调用,但此时 ts 没有及时给出报错

在这种情况下我们就可以对Box对象进行泛型声明

可以这样理解下面的声明:Box的Type就是content属性的类型

interface Box<Type> {  
    contentType;  
}

然后重点是我们在引用Box类型的时候需要给出Type的具体类型,比如:

const boxBox<string> = {  
    content'string value'  
}

这样TS会明确知道box.content是string类型,从对box.content的调用做出准确的检查:

WechatIMG280.jpeg

另外我们还可以用type来声明泛型:

type Box<Type> = {  
    contentType;  
}

同时因为type不仅可以声明对象类型,我们还能用type来声明一些泛型的辅助类型,例如:

type OrNull<T> = T | null;  
  
type OneOrMany<T> = T | T[];  
  
type OneOrManyOrNull<T> = OrNull<OneOrMany<T>>;  
  
// 应用  
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

实用工具类型

文章的最后我们来认识一些实用的工具类型吧🔧

  1. Partial< Type>

返回一个与Type属性相同但全被设为可选的新类型:

interface Todo {  
    title: string;  
    desc: string;  
}  
  
let optionalTodoPartial<Todo>;  
/**  
   {  
       title?: string;  
       desc?: string;  
   }  
*/
  1. Required< Type>

与Partial相反,返回一个与Type属性相同但全被设为必填的新类型:

interface Info {  
    name?: string;  
    age?: number;  
}  
  
let requiredInfoRequired<Info>;  
/**  
    {  
       name: string;  
       age: number;  
    }  
*/
  1. Pick<Type,Keys>

从Type里挑出指定的Keys来构造一个新类型:

interface Todo {  
    title: string;  
    desc: string;  
    completed: boolean;  
}  
  
type TodoPreview = Pick<Todo'title' | 'completed'>;  
/**  
    {  
       title: string;  
       completed: boolean;  
    }  
*/
  1. Omit<Type,Keys>

与Pick相反,从Type里移除掉指定的Keys来构造一个新类型:

interface Todo {  
    title: string;  
    desc: string;  
    completed: boolean;  
}  
  
type TodoPreview = Omit<Todo'desc' | 'completed'>;  
/**  
    {  
       title: string;  
    }  
*/
  1. Extract< UnionType,ExtractedMembers>

取UnionType和ExtractedMembers的交集来构造一个新类型:

type T0 = Extract<'a' | 'b' | 'c''a' | 'f'>; // T0: 'a'  
  
type T1 = Extract<string | number | (() => void), Function>; // T1: () => void
  1. Exclude<UnionType,ExcludedMembers>

从UnionType里移除掉ExcludedMembers存在的类型来构造一个新类型:

type T0 = Exclude<'a' | 'b' | 'c''a'>; // T0: 'b' | 'c'  
  
type T1 = Exclude<string | number | (() => void), Function>; // T1: string | number
  1. NonNullable< Type>

从Type里移除掉undefined和null来构造一个新类型:

type T0 = NonNullable<string | number | undefined | null>; // T0: string | number