初识TypeScript

280 阅读13分钟

什么是TypeScript

TypeScript 是由微软开发的开源编程语言。它是JavaScript 的一个超集,本质上是将这些类型附着在原 JavaScript 的语言之上,给其加上类型限制使得其静态化,能够在他们的 IDE 中或在编译时而不是在运行时捕获错误,从而降低将错误传送到生产中的风险,更适合团队协作的代码,提高代码编译质量。

总结概括: 在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型系统来提高代码的可维护性,减少可能出现的 bug。

TypeScript工作流程

Install

npm install -g typescript

使用方法 :

  • 创建.ts文件
  • 编译.ts文件:tsc 文件名.ts —> 生成对应的.js文件
  • 运行.js文件

ts-node安装

我们想直接运行 TS 文件并查看结果时, 可以借助 ts-node 插件

全局安装

npm install -g ts-node

找到文件路径,运行即可

ts-node index.ts

✨ TypeScript Playground 可以直接开启TypeScript之旅,通过配置 TS Config 的 Target,可以设置不同的编译目标,从而编译生成不同的目标代码 👍 👍 👍

变量声明

TypeScript变量声明时需要指定对应的数据类型

let 变量名: 数据类型 = 值

数据类型

🌰 boolean 类型

//最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫boolean

let isDone: boolean = false



🌰 number 类型

//和JavaScript一样,TypeScript里的所有数字都是浮点数。这些浮点数的类型是number

let count: number = 10



🌰 string 类型

//string表示字符串类型,使用双引号("")或单引号('')

let name: string = "Holo"

undefinednull 是所有类型的子类型。可以赋值给任意类型的变量

//还可以使用模版字符串,可以定义多行文本和内嵌表达式。这种字符串是被反引号包围(``)

let name: string = `${Holo}`//是变量



🌰 Array 类型

//两种方式

//第一种,在元素类型后面接上[],表示由 此类型元素,组成的一个数组

let list: number[] = [1, 2, 3];//表示数字类型的数组,所有元素一定都是数字组成(推荐),若需要有多种类型可以使用联合类型定义数据形式

//第二种,使用数组泛型,Array<元素类型>:

let list: Array<number> = [1, 2, 3]



🌰 Tuple(元组)类型

//元组类型允许你用固定数量的元素表示数组,这些元素的类型是已知的,但不必相同。

//比如,你可以定义一对值分别为 string和number类型的元组



// 声明元组类型

let a: [string, number];

// 正确的初始化

a = ["hello", 10]; 

// 错误的初始化

a = [10, "hello"]; 



🌰 enum(枚举)类型

//枚举是组织收集有关联变量的一种方式,可以定义一些带名字的常量,TypeScript支持数字的和基于字符串的枚举

数字枚举

//在仅指定常量命名的情况下,定义的就是一个默认从 0 开始递增的数字集合,称之为数字枚举。如果想要从其他值开始递增,可以将第一个值的索引值进行指定

enum Person {

  name = 1,

  age, 

  job,

} 

console.log(Person.name); // console.log(Person.name); // 1

// 数字枚举,可自定义顺序

enum Person {

 name = 4, 

 age = 3, 

 job = 2 

} 

console.log(Person.name); // console.log(Person.name); // 4



字符串枚举

//TypeScript将定义值是字符串字面量的枚举称为字符串枚举,字符串枚举值要求每个字段的值都必须是字符串字面量,或者是该枚举值中另一个字符串枚举成员

// 使用字符串字面量

enum Message {

  Error = "Sorry, error",

  Success = "Hoho, success"

}

console.log(Message.Error); // 'Sorry, error'



// 使用枚举值中其他枚举成员

enum Message {

  Error = "error message",

  ServerError = Error,

  ClientError = Error

}

console.log(Message.Error); // 'error message'

console.log(Message.ServerError); // 'error message'



🌰 any 任意类型(代表所有类型)//任意值,是ts针对编程时类型不明确的变量使用的一种数据类型

let value: any = 1; // 数字类型

value = 'I am who I am'; // 字符串类型

value = false; // 布尔类型



const foo: any = 123;

const value1: any = foo;

const value2: unknown = foo;

const value3: string = foo;

console.log('value1', value1); // OK

console.log('value2', value2); // OK

console.log('value3', value3); // OK

//any类型的值执行操作之前,不进行任何检查。



🌰 Unknown类型 //表示未知类型,也可以把任何值赋值给unknown,但是不能调用属性和方法

//unknown类型只能被赋值给any类型和unknown类型本身

const bar: unknown = 123; // OK

const value1: unknown = bar;

const value2: any = bar;

const value3: string = bar;

console.log('value1', value1); // OK

console.log('value2', value2); // OK

console.log('value3', value3); // Error



const value:unknown;

value = 1

value.length // 报错



更改: 调用属性和方法,需要类型断言

const value:unknown;

value = 'hello';

(value as string).length



//any与Unknown区别:

//unknown类型会更加严格:在对unknown类型的值执行大多数操作之前,必须进行某种形式的检查, 

//unknown因为未知性质,不允许访问属性,不允许赋值给其他有明确类型的变量



🌰 void类型

//与any类型相反,表示没有任何类型, 当函数没有返回值时,可以用void表示

function fun(): void { // 函数没有返回值

  // return null

}

//声明一个void类型的变量没什么用处,因为在严格模式下,它的值只能为undefined

let value1: void = undefined

let value2: void = 'hello world'// void 类型不能赋值为字符串及其他基本类型



🌰 NullUndefined 类型

// null和 undefined两者有各自的类型分别为null和undefined。

//undefined 是没有赋值变量的默认值,自动赋值;

//null:释放一个变量的引用对象,表示一个变量不在指向任何对象地址。

共同点:

//都是原始数据类型,保存在栈中变量本地

不同点:

//undefined:表示变量声明过,但未必赋过值,而且它是所有未赋值变量的默认值;

//null:表示一个变量将来可能指向一个对象,通常用于主动释放指向对象的引用

let test1: undefined = undefined

let test2: null = null

let test3: number

let test4: string

test3 = null // 报错, 不能将类型“null”分配给类型“number”

test4 = undefined // 报错, 不能将类型“undefined”分配给类型“string”





🌰 Never类型

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

//例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型

function foo(): never {

 return throw new Error('never reach') //throw error返回值是never

}

//特性:1.函数中调用了返回 never 的函数后,之后的代码都会变成deadcode

function test() {

  foo();   // foo指上面返回never的函数

  console.log(111); // Error: 编译器报错,此行代码永远不会执行到

}

//2.无法把其他类型赋给never

let n: never;

let o: any = {};

n = o;  // Error: 不能把一个非never类型赋值给never类型,包括any



🌰 联合类型

//数组中既有 number 类型,又有 string 类型,这时数组的类型应该如何写?

let arr: (number | string)[] = [1, 'a', 3, 'b']

// | (竖线)在TS中叫联合类型,即:由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种

函数声明

// JS

function func1(x, y) {

  return x + y

}

// TS

//⚠️ 需要定义传入参数的类型, 

function func2(x: number, y: number): number {

  return x + y

}

TypeScript断言

有时会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

类型断言

类型断言(Type Assertion)可以手动指定一个值的类型,即允许变量从一种类型更改为另一种类型, 允许你覆盖它的推断,并且能以你任何你想要的方式分析它,这种机制被称为类型断言

类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。

  1. "尖括号"语法
//定义一个any类型的变量, 但我们明确知道这个变量中保存的是字符串类型,

//通过类型断言将any类型转换成string类型, 使用字符串类型中相关的方法

let value1: any = 'hello'

let value1Length: number = (<string>value1).length
  1. as语法
let value1: any = 'world'

let value1Length: number = (value1 as string).length



//函数式

function foo (key: string | null) {

 const value2 = key; 

 console.log(value2.length); //提示报错,对象可能为 "null" 所以此时length可能不存在,编译不通过

 const value3 = key; 

 console.log((value3 as string).length); //通过类型推断出value3为string,length属性存在,编译通过

}

非空断言

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined

使用场景:

1.忽略undefinednull类型

function myFunc(maybeString: string | undefined | null) {

  // Type 'string | null | undefined' is not assignable to type 'string'.

  // Type 'undefined' is not assignable to type 'string'. 

  const onlyString: string = maybeString; // Error

  const ignoreUndefinedAndNull: string = maybeString!; // Ok

}



2.调用函数时忽略 undefined

type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {

  // Object is possibly 'undefined'.(2532)

  // Cannot invoke an object which is possibly 'undefined'.(2722)

  const num1:number = numGenerator(); // Error

  const num2:number = numGenerator!(); //OK

}

确定赋值断言

在 TypeScript 2.7 版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 ! 号,从而告诉ts 该属性会被明确地赋值。

let x: number; //未赋值

initialize();

// Variable 'x' is used before being assigned.(2454)

console.log(2 * x); // Error



function initialize() {

  x = 10;

}

很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:

let x!: number;

initialize();

console.log(2 * x); // Ok



function initialize() {

  x = 10;

}

总结:确定赋值断言是变量声明时用,而非空断言是使用变量时用。

类型别名

类型别名 type 可以给一个类型起个新名字,当命名基本类型或联合类型等非对象类型时非常有用

type MyNumber = number; //基本类型

 type StringOrNumber = string | number; // 联合类型

 type myType = {  // 描述一个对象的类型,名称唯一,不能重复 

     name: string 

     age: number 

 } 

 const obj: myType = { 

     name: 'ccc',

     age: 33  // 这里的属性要和类型声明中保持一致

  }

通过 type 关键字为对象字面量类型取了一个别名,从而方便在其他地方使用。

接口Interface

接口是对象结构的描述,接口用关键字定义的 interface,它可以包含使用函数或箭头函数的属性和方法声明, 起到限制和规范的作用。它只规定这批类里必须提供某些方法,提供这些方法的类就可以满足实际需要, 简单来说,接口也是在定义标准,更加灵活和全面。

interface IPerson { //接口首字母通常大写,并附带大写I,表示是接口类型


    firstName: string;

    lastName: string;

}

function greeter(person: IPerson) {

    return "hello" + person.firstName + "" + person.lastName;

}

let user = {

   firstName: "xunna",

   lastName: "su",

};

console.log("object :>> ", greeter(user));



//其他属性

interface IPerson {

 readonly name:string, //readonly表示只读

 age?:number //?可选属性,表示可有可无

 [propName: string]:any //任意属性, 支持扩展额外属性的检查

 say():string //返回值为 string 的函数

}



// 举个 🌰

interface IPerson {

    name: string;

    age?: number; // ERROR 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

    [propName: string]: string | number; //一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型

    //修改为 [propName: string]: string | number| undefined

}

let tom: Person = {

    name: 'Tom',

    age: 25,

    gender: 'male'

};

接口的继承

//接口继承接口

//接口可以相互继承, 让我们能从一个接口里复制成员到另一个接口里,更灵活地将接口分割到可复用的模块里。

interface Alarm {

    alert(): void;

}

interface LightableAlarm extends Alarm {

    lightOn(): void; 

    lightOff(): void;

}

//很好理解,LightableAlarm 继承了 Alarm,

//除了拥有alert方法之外,还拥有两个新方法 lightOn 和 lightOff。

函数类型接口

//基本语法


interface 函数接口名{

    (参数列表): 函数返回值}


}





//获取最大值和最小值之间的随机数

interface Irandom {

    (min: number, max: number): number;

}

let getRandom: Irandom = function (max, min) {

    return Math.random() * (max - min) + min;

};

console.log("getRandom", getRandom(0, 100));

这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。

可索引类型接口

用来对数组和对象进行约束,与使用接口描述函数类型差不多,描述那些能够“通过索引得到”的类型,可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型

interface StringArray {

  [index: number]: string;

}



let myArray: StringArray;

myArray = ["Bob", "Fred"]; 

let myStr: string = myArray[0]; // Bob

函数重载

函数重载是一个同名函数完成不同的功能,编译系统在编译阶段通过函数参数个数、参数类型不同,函数的返回值来区分该调用哪一个函数,即实现的是静态的多态性

简单理解: 同一个函数,根据传递的参数不同,会有不同的表现形式, 让你清晰的知道传入不同的参数得到不同的结果.

function fun(x: number | string): number | string {

    if (typeof x === 'number') {

        return x * 2;

    } else {

        return x + ', ' + x;

    }

}

console.log(fun(1).toFixed(2))  // 报错

console.log(fun('a').length) // 同理报错



//函数重载

function fun(x: number): number;

function fun(x: string): string;

function fun(x: number | string): number | string {

    if (typeof x === 'number') {

        return x * 2;

    } else {

        return x + ', ' + x;

    }

}

console.log(fun(1).toFixed(2))

console.log(fun('a').length)

通过编写多套重载签名,来规定函数的不同调用方式 (传入不同数量或不同类型的参数以及不同类型的返回值)。然后通过实现签名来进行兼容的逻辑处理

重载签名和实现签名

// 定义两套重载签名

// 允许调用函数时只传入name参数

function setUserInfo(name: string): boolean;

// 允许调用函数时传入name, age, gender三个参数

function setUserInfo(name: string, age: number, gender: 1 | 2): string;

// 实现签名,统一处理逻辑

function setUserInfo(name: string, age?: number, gender?: 1 | 2): string | boolean {

    // 真值校验,由于两套重载签名规定,调用函数时要传入一个或三个参数

    if (age) {

        return `我叫 ${name}, 今年 ${age} 岁!`;

    } else {

        return false;

    }

}

// 传一个参数,正确

setUserInfo("cc");

// 传三个参数,正确

setUserInfo("cc", 18, 2);

// 传两个参数,报错,因为没有定义两个参数的重载签名

setUserInfo("cc", 18);

⚠️注意: 函数重载是从上往下匹配,若有多个函数定义,有包含关系的,要把精确的,写在最前面。

规定函数的形参与返回值的是重载签名,可以有多个重载签名;

兼容多个重载签名并进行逻辑处理的是实现签名,由于要兼容多套重载签名,因此会出现可选参数;

泛型

泛型表示泛指某一种类型,可以指定一个表示类型的变量,用它来作为实际类型的占位符,用尖括号来包裹类型变量 <T>

泛型的主要作用是创建可重用的组件,从而让一个组件可以支持多种数据类型,它可以作用在接口、类、函数或类型别名上,它可以不预先指定具体的类型,而在使用的时候再指定类型的一种特性。解决对不特定数据类型的支持(类型校验),通常用 T 表示, 但不是必须使用该字母,只是常规,还有其他常用字母

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

泛型是所有类型中的一种类型,并不是完全的所有类型,所以在函数中返回的类型要和定义的类型一模一样,不能修改他原有类型

泛型函数 (实现传入什么类型返回什么类型)

function identity <T> (value:T):T{

  // 传入即返回

  return value;

}

console.log(identity<number>(1)); // 返回值是number类型的 1

console.log(identity<string>("holo")); //返回值是string类型的 holo

console.log(identity<boolean>(true)); //返回值是布尔类型的 true

//当然也可以返回任意类型any

多个类型变量

开发中,我们可能希望定义的任何数量的类型变量。比如我们引入一个新的类型变量U,用于扩展我们定义的identity函数

其实编译器会自动选择定义好的类型,并将它们赋值对应的变量。可以完全省略函数调用时的尖括号

泛型接口

首先让我们创建一个用于的identity函数通用Identities 接口:

interface Identities<V, M> {

  value: V,

  message: M

}

//将Identities接口作为identity函数的返回类型

function identity<T, U> (value: T, message: U): Identities<T, U> {

  let identities: Identities<T, U> = {

    value,

    message

  };

  return identities;

}

console.log(identity(68, "Semlinker")); // {value: 68, message: "Semlinker"}

泛型除了可以应用在函数和接口之外,它也可以应用在类中,下面看一下在类中如何使用泛型

泛型类

泛型类与泛型接口差不多, 泛型类使用 括起泛型类型,跟在类名后面

class Stack<T> {


    //private 定义为私有属性,只能在类的内部访问,外部访问会报错。

    private data: T[] = [];

    push(item: T) {

        return this.data.push(item);

    }

}

const t1 = new Stack<number>();

console.log("t1.push(89)", t1.push(0)); //返回长度 1

泛型约束

有时我们希望限制每个类型变量接受的类型数量,这就是泛型约束。下面我们来举几个例子,介绍一下如何使用泛型约束

确保属性存在: 有时我们希望类型变量对应的类型上存在某些属性。这时,除非我们显式地将特定属性定义为类型变量,否则编译器不会知道它们的存在。

function identity<T>(arg: T): T {

  console.log(arg.length); // Error

  return arg;

}

//编译器将不会知道T确实含有length属性,尤其是在可以将任何类型赋给类型变量 T 的情况下



//我们需要做的就是让类型变量 extends 一个含有我们所需属性的接口,比如这样:

interface Length {

  length: number;

}



function identity<T extends Length>(arg: T): T {

  console.log(arg.length); // 成功获取length属性为2

  return arg;

}

identity([1,2])

检查对象上的键是否存在

泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。在检查之前,我们先了解下 keyof操作符,keyof是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型

interface Person {

  name: string;

  age: number;

  location: string;

}

type K1 = keyof Person; // "name" | "age" | "location"

keyof操作符得到的是 Person 类型的所有键值类型即 'name','age','location' 三个字面量类型组成的联合类型 'name' | 'age' | 'location'

通过keyof 操作符,可以获取指定类型的所有键,之后我们就可以结合前面介绍的extends约束,即限制输入的属性名包含在keyof返回的联合类型中。具体的使用方式如下

//keyof增强函数类型功能


function getValue<T extends Object, K extends keyof T>(o: T, key: K): T[K] {

    return o[key];

}

const obj1 = { name: "张三", age: 12 };

const result = getValue(obj1, "");// 仅展示可能有的属性

const result = getValue(obj1, "name");//ok

console.log("result:>> ", result);

泛型别名

类型别名会给一个类型起个新名字。 类型别名和接口很像,但是可以作用于原始值,联合类型,元祖以及其它任何你需要手写的类型

interface Person {

  name: string;

  age: number;

}

const jack: Person = { name: 'jack', age: 100 };

type Jack = typeof jack; // -> Person

泛型接口 VS 泛型别名

继承, 合并

  • interface可以用extends继承,同时定义的接口能用于类实现,而type不行。
  • interface能够声明合并,而type不行。
//interface使用extends继承;

interface Name {

    name: string;

}

interface User extends Name {

    age: number;

}

//interface合并

interface yourType {

    name: string

    age: number

}

interface yourType {

    gender: string

}

const you: yourType = {

    name: 'sna',

    age: 10,

    gender: '女'   // 这里的属性要和接口中一致,不能多,不能少

}

命名规范

  • type类型别名,命名唯一,不可重复
  • interface接口 可以重复定义
// 类型声明,名称唯一,不可重复

type typeA = {

    name: string,

    age: number

}

const typeB: typeA = {

    // 这里的属性要和类型声明中一致,不能少,不能多

    name: 'sxn',

    age: 18

}



interface something  {

    name: string,

    age: number

}

interface something  {

    level: number

}

const objs: something = {

    name: 'zs',

    age: 18,

    level: 3

}

type可以使用 in 关键字生成映射类型, interface不行

in用于取联合类型的值,主要用于数组和对象的构造。

type Keys = "firstname" | "surname"



type DudeType = {

  [key in Keys]: string

}



const test: DudeType = {

  firstname: "Pawel",

  surname: "Grzybek"

}

interface 与 type 可以相互扩展对方

type PartialPoint = { x: number; };

interface Point extends PartialPoint { y: number; } // 利用type扩展interface

const obj: Point = {

    x: 22,

    y: 33

}



interface PartialPoint { x: number; }

type Point = PartialPoint & { y: number; }; // 利用interface扩展 type

const obj: Point = {

    x: 22,

    y: 33

}

console.log(obj) 

// 输出:{ "x": 22, "y": 33 }

相同点:

  • 都可以描述一个对象或者函数
  • 都允许拓展(extends和交叉类型)

不同点:

  • type 可以声明基本类型别名,联合类型,元组等类型
  • type 语句中还可以使用 typeof 获取实例的 类型进行赋值
  • interface 能够声明合并

泛型好处:

  • 函数和类支持多种类型,增强程序的拓展性。
  • 不必写冗长的联合类型,增强代码的可读性。
  • 灵活控制类型之间的约束。

何时使用泛型:

  • 当函数、接口或类将处理多种数据类型时。
  • 当函数、接口或类在多个地方使用该数据类型时。
  • 当我们需要使用联合类型或者元组类型的时候,类型别名会更合适

总结:

  • 用interface描述数据结构,用type描述类型关系
  • interface只能用于对象或者函数类型定义,而不能对基础类型进行定义,但是对定义联合类型还是很有用的
  • 公共的用 interface 实现,不能用 interface 实现的,再用 type 实现

泛型工具类型

1.Partial

partial的作用就是将某个类型中的属性全部变为可选项?

interface Person {

  name:string;

  age:number;

}

function student<T extends Person>(arg: Partial<T>):Partial<T> {

  return arg;

}

console.log(student({name:"a"})) // ok

2.Record

type proxyKType = Record<K,T>

Record的作用是将 K 中所有属性值转化为 T 类型,并将返回的新类型返回给proxyKType,K可以是联合类型、对象、枚举等,我们常用它来申明一个普通 object 对象,来表示对象键和值的类型, 即一个对象的key和value类型

//创建一个状态值映射对象, 兑现的成员是每一个状态值,成员的值是String类型。

type state = "created" | "submitted" 



//普通写法

interface StatesInterface {

  created:string

  submitted:string

}

export const states:StatesInterface = {

  created:'01',

  submitted:'02',

}



//使用Record

export const states:Record<state,string> = {

  created:'01',

  submitted:'02',

}

3 Pick

Pick<T, K extends keyof T>的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型,示例:

interface Todo {

  title:string,

  desc:string,

  time:string

}

type TodoPreview = Pick<Todo, 'title'|'time'>;

const todo: TodoPreview = {

  title:'a'

  time:'2022-08-12'

}

4.Exclude

Exclude<T,U>的作用是将某个类型中属于另一个的类型移除掉

type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c" 

type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"

5.ReturnType

returnType<T>的作用是用于获取函数 T 的返回类型。

function foo(x: string | number): string | number { /*..*/ }

type FooType = ReturnType<foo>;  // string | number

6.Required

Required作用是可以将类型 T 中所有的属性变为必选项。

type Required<T> = {

  [P in keyof T]-?: T[P]

}

有趣的语法-?,理解为就是 TS 中把 ? 可选属性减去的意思。

🤔️ 项目中的一些思考

Ts类型解决了开发中哪些问题?

  • 代码自动检查, 我们在编译阶段就能发现错误,尤其是从vue2更新到vue3的时候,要用value获取具体值的时候。
  • 接口请求和响应类型, 让我们能更准确的处理数据的方法 ,不需要写太多的?. 或者&&判断
  • 重构代码的时候更方便, 因为改动了interface后,相关联的地方都会报错,让你能及时同步到

TS 能让人养成“先思考后动手”的好习惯

在以往的开发过程中,我们习惯总是“先想好一个大概,然后边做边想再边改”。这样的好处是动作比较快,顺利的时候效率会很高,但更多的时候是不断地推翻自己先前的想法,相信不少的人也有跟我类似的体会。

而使用 ts,可以在一定程度上减少这个问题。众所周知 ts 是强类型的语言,这也意味着它能有效制约开发者在开发过程中“随心所欲”的程度。

TS 拥有自成文档的特性

在写 js 的时候,我们依赖注释去判断某个变量或参数的类型、结构和作用。如果没有了注释,只能通过阅读源码和不断调试去搞清楚当中的细节。不仅浪费时间还会让接手项目的人很崩溃

在 ts 中,除了注释以外我们还有另外一个选择,就是查看某个变量或参数所对应的 interface接口定义。在 interface中我们可以很直观地看到参数的结构,内部属性的类型,是否为可选等详细信息。再加上VScode 的智能提示及跳转,不管是查看他人的代码还是维护一个历史项目,都能更加方便和规范——毕竟写接口往往比写注释要顺手,看接口往往比猜代码要稳妥。

自成文档的特性对于多人维护的项目来说是非常有用的,它能够大大降低项目当中沟通和理解的成本。但是这句话也有一个前提,就是开发者要遵守并合理工具当中的约束规范,如果一个接口的任何参数类型都是 any ,那么也就失去了使用 ts 的意义。

偏好使用interface还是type 定义类型

从用法上来说两者本质上没有区别,从扩展的角度来说,type 比 interface 更方便拓展一些,假如有以下两个定义:

type Name = { name: string };

interface IName { name: string };

想要做类型的扩展的话,type 只需要一个&,而 interface 要多写些代码

type Person = Name & { age: number };

interface IPerson extends IName { age: number };

另外 type 有一些 interface 做不到的事情,比如使用|进行枚举类型的组合,使用typeof获取定义的类型等等。

不过 interface 有一个比较强大的地方就是可以重复定义添加属性,比如我们需要给window对象添加一个自定义的属性或者方法,那么我们直接基于其 Interface 新增属性就可以了。

declare global {

    interface Window { MyNamespace: any; }

}

总体来说,大家知道 TS 是类型兼容而不是类型名称匹配的,所以一般不需用面向对象的场景或者不需要修改全局类型的场合,我一般都是用 type 来定义类型。

是否允许any 出现

说实话,刚开始使用 TS 的时候还是挺喜欢用 any 的,毕竟大家都是从 JS 过渡过来的,对这种影响效率的代码开发方式并不能完全接受,因此不管是出于偷懒还是找不到合适定义的情况,使用 any 的情况都比较多。

随着使用时间的增加和对 TS 学习理解的加深,逐步离不开了 TS 带来的类型定义红利,不希望代码中出现 any,所有类型都必须要一个一个找到对应的定义,甚至已经丧失了裸写 JS 的勇气。

这是一个目前没有正确答案的问题,总是要在效率和时间等等因素中找一个最适合自己的平衡。不过我还是推荐使用 TS,随着前端工程化演进和地位的提高,强类型语言一定是多人协作和代码健壮最可靠的保障之一,多用 TS,少用 any,也是前端界的一个普遍共识。

以上,如有谬误,还请斧正。

最后,希望这篇文章对你有所帮助,感谢您的阅读~