如果评定前端在最近五年的重大突破,Typescript肯定能名列其中,重大到各大技术论坛、大厂面试都认为Typescript应当是前端的一项必会技能。作为一名消息闭塞到被同事调侃成“新石器时代码农”的我,也终于在2019年底上车了Typescript。使用的一年间整理了许多的笔记和代码片段,花了一段时间整理成了下文。

本文不是教程,主要目的是分享我个人在使用Typescript开发1年期间的一些理解和代码片段,因此文章内容主要围绕对某些特性做的研究和理解。也希望能帮到一些同在学习使用Typescript的小伙伴,如有错误遗漏也希望能够指出。

基础数据类型

Javascript一共有6种基础类型:String/Number/Boolean/Null/Undefined/Symbol,分别对应Typescript中6种类型声明:string/number/boolean/null/undefined/symbol

基础数据类型的类型声明适用的几条规则:

  1. Typescript在编译时会对代码做静态类型检查,多数情况下不支持隐式转换,即let yep: boolean = 1会报错
  2. Typescript中的基础类型声明的首字母不区分大小写,即let num: number = 1等同于let num: Number = 1,但是推荐小写形式
  3. Typescript允许变量有多种类型(即联合类型),通过|连接即可,如let yep: number | boolean = 1,但是不建议这么做
  4. 类型声明不占用变量,因此let boolean: boolean = true是允许的,但是不建议这么用
  5. 默认情况下,除了neverTypescript可以把其他类型声明(包括引用数据类型)的变量赋值为null/undefined/void 0而不报错。但这肯定是错误的,建议在tsconfig.json中设置"strictNullChecks": true屏蔽掉这种情况
  6. 对于基础类型而言,unknownany的最终结果是一致的
// 字符串类型声明,单引号/双引号不影响类型推断
let str: string = 'Hello World'; // 数字类型声明
let num: number = 120;
// 这些值也是合法的数字类型
let nan: number = NaN;
let max: number = Infinity;
let min: number = -Infinity; // 布尔类型声明
let not: boolean = false;
// Typescript只对结果进行检查,!0最后得到true,因此不会报错
let yep: boolean = !0; // symbol类型声明
let key: symbol = Symbol('key'); // never类型不能进行赋值
// 执行console.log(never === undefined),执行结果为true
let never: never;
// 但即使never === undefined,赋值逻辑仍然会报错
never = undefined; // 除了never,未开启strictNullChecks时,其他类型变量赋值为null/undefined/void 0不报错
let always: boolean = true;
let isNull: null = null;
// 不会报错
always = null;
isNull = undefined;

引用数据类型

Javascript的引用数据类型有很多,比如Array/Object/Function/Date/Regexp等,与基础类型不一样的地方是,Typescript有些地方并不能简单地与Javascript直接对应,部分的执行结果让人摸不着头脑。

在书写规则上,除了Object以外,Typescript其他的引用数据类型声明的首字母必须大写,如let list: array<number> = [1]会报错,必须写成let list: Array<number> = [1]。原因是这些引用数据类型在本质上都是构造函数,Typescript的底层会通过类似于list instanceof Array的逻辑进行类型比对。

其中比较有意思的一个点是:在所有的数据类型里,Array是唯一的泛型类型,也是唯一有两种不同的写法:Array<T>T[]

与数组相关的类型声明还有元组Tuple,跟数组的差别主要体现在:元组的长度是固定已知的。因此使用场景也非常明确,适合用在有固定的标准/参数/配置的地方,比如经纬度坐标、屏幕分辨率等。

// 数组类型有Array<T>和T[]两种写法
let arr1: Array<number> = [1]
let arr2: number[] = [2] // 未开启strictNullChecks时,赋值为null/undefined/void 0不报错
let arr3: number[] = null
// 编译时不会报错,运行时报错
arr3.push(1) // 元组类型
// 坐标表示
let coordiate: [ number, number ] = [114.256429,22.724147] // 其他引用数据类型
let date: Date = new Date()
let pattern: Regexp = /\w/gi // 类型声明在函数中的简单运用
// 函数表达式的写法
function fullName(firstName: string, lastName: string): string {
return firstName + ' ' + lastName
}
// 函数声明式的写法
const sayHello = (fullName: string): void => alert(`Hello, ${ fullName }`) // 当你不知道函数的返回值,但又不想用any/unknown的时候可以试试这种类型声明的写法,不过不推荐
const sayHey: Function = (fullName: string) => alert(`Hey, ${ fullName }`)

Typescript中关于对象的类型声明一共有三种形式:Object/object/{},我一开始以为Object会像Array也是泛型类型,然而经过测试发现不仅不是泛型,还有个首字母小写形式的objectObject/object/{}三者之间的执行结果完全不同。

  1. Object作为类型声明时,变量值可以是任意值,如字符串/数字/数组/函数等,但是如果变量值不是对象,则无法使用其变量值特有的方法,如let list: Object = []不会报错,但执行list.push(1)会报错。造成这种情况的原因是因为在Javascript中,在当前对象的原型链上找不到属性/方法时,会向上一层对象进行查找,而Object.prototype是所有对象原型链查找的终点,也因此在Typescript中将类型声明成Object不会报错,但无法使用非对象的属性/方法

  2. object作为类型声明时,变量值只能是对象,其他值会报错。值得注意的是,object声明的对象无法访问/添加对象上的任何属性/方法,实际效果类似于通过Object.create(null)创建的空对象,暂时不知道这么设计的原因

  3. {}其实就是匿名形式的type,因此支持通过&|操作符对类型声明进行扩展(即交叉类型和联合类型)

// 赋值给数字不会报错
let one: Object = 1
// 也赋值给数组,但无法使用数组的push方法
let arr: Object = []
// 会报错
arr.push(1) // 赋值会报错
let two: object = 2 // object作为类型声明时,赋值给对象时不会报错
let obj1: object = {}
let obj2: object = { name: '王五' }
let Obj3: Object = {} // 会报错
obj1.name = '张三'
obj1.toString()
obj2.name // 不会报错
Obj3.name = '李四'
Obj3.toString() // {} 等同于匿名形式的type
type UserType = { name: string; } let user: UserType = { name: '李四' }
let data: { name: string; } = { name: '张三' }

交叉类型和联合类型

上文提到,Typescript支持通过&|操作符对类型声明进行扩展,用&相连的多个类型是交叉类型,用|相连的多个类型是联合类型。

两者之间的区别主要体现在联合类型主要在做类型的合并,如Form4TypeForm6Type;而交叉类型则是求同排斥,如Form3TypeForm5Type。也可以用数学上的合集和并集来分别理解联合类型和交叉类型。


type Form1Type = { name: string; } & { gender: number; }
// 等于 type Form1Type = { name: string; gender: number; }
type Form2Type = { name: string; } | { gender: number; }
// 等于 type Form2Type = { name?: string; gender?: number; } let form1: Form1Type = { name: '王五' } // 提示缺少gender参数
let form2: Form2Type = { name: '刘六' } // 验证通过 type Form3Type = { name: string; } & { name?: string; gender: number; }
// 等于 type Form3Type = { name: string; gender: number; }
type Form4Type = { name: string; } | { name?: string; gender: number; }
// 等于 type Form4Type = { name?: string; gender: number; } let form3: Form3Type = { gender: 1 } // 提示缺少name参数
let form4: Form4Type = { gender: 1 } // 验证通过 type Form5Type = { name: string; } & { name?: number; gender: number; }
// 等于 type Form5Type = { name: never; gender: number; }
type Form6Type = { name: string; } | { name?: number; gender: number; }
// 等于 type Form6Type = { name?: string | number; gender: number; } let form5: Form5Type = { name: '张三', gender: 1 } // 提示name的类型为never,不能进行赋值
let form6: Form6Type = { name: '张三', gender: 1 } // 验证通过

上述的代码片段一般只会在面试题里面出现,如果这种代码出现在真实的项目代码里面,估计在代码评审的时候就直接被点名批评了。

不过也不是没有实用场景,以苹果的教育优惠举个例子:假设原价购买苹果12需要5000元;如果通过教育优惠购买则可以享受一定折扣的优惠(比如打8折),但是需要提供学生证或者是教师证。经过产品经理的整理,转变为需求文档之后可能就变成了:原价购买无需其他材料,如需享受教育优惠,则需要提交个人资料以及学生证/教师证扫描件。

// 原价购买
type StandardPricing = {
mode: 'standard';
}
// 教育优惠购买需要提供购买人姓名和相关证件
type EducationPricing = {
mode: 'education';
buyer_name: string;
sic_or_tic: string;
}
// 通过&和|合并类型
type buyiPhone12 = { price: number; } & ( StandardPricing | EducationPricing ) let standard: buyiPhone12 = { mode: 'standard', price: 5000 }
let education: buyiPhone12 = { mode: 'education', price: 4000, buyer_name: '张三', sic_or_tic: '证件' }

Type和Interface

在一开始学习Typescript的时候看到interface,我第一时间想到的是JavaJavainterface是一种抽象类,把功能的定义和具体的实现进行分离,方便不同人员可以通过interface进行相互配合,类似于需求文档在开发中的作用。

// 张三定义了用户中心的功能有三个:登录、注册、找回密码
interface UserCenterDao {
void userLogin();
void userRegister();
void userResetPassword();
} // 李四开发用户中心的功能就会提示需要实现三个功能
class UserCenter implements UserCenterDao {
public void userLogin() {};
public void userRegister() {};
public void userResetPassword() {};
}

Typescript对于interface的定义也是类似,都是声明一系列的抽象变量/方法,然后通过具体的代码去实现。

interface整体的效果与用type声明的效果非常相似,即使是专属于interface的继承extendstype也可以通过&|操作符实现,两者之间也不是独立的,也可以互相进行调用。

因此在平时的实际开发中,不必太过纠结使用type还是interface进行类型的声明,特别纠结的时候type一把梭。

// 用interface定义一个学生的基础属性为姓名、性别、学校、年级、班级
interface Student {
name: string;
gender: '男' | '女';
school: string;
grade: string | number;
class: number;
} // 用interface继承学生的基础属性
// 并追加定义三好学生的标准为遵守校规、乐于助人,班级前三
interface MeritStudent extends Student {
toeTheLine: boolean;
helpingOther: boolean;
topThreeInClass: boolean;
} // 可以通过type将interface声明的类型声明到新声明上
type StudentType = Student // interface虽然不能直接使用type声明的类型,但是可以通过继承间接使用
interface CollageStudent extends StudentType {} // 然后声明相对应的逻辑去实现
let xiaoming: Student = {
name: '小明',
gender: '男',
school: '清华幼儿园',
grade: '大大班',
class: 1
} let xiaowang: MeritStudent = {
name: '小王',
gender: '男',
school: '清华幼儿园',
grade: '大大班',
class: 1,
toeTheLine: true,
helpingOther: true,
topThreeInClass: true
} let xiaohong: StudentType = {
name: '小红',
gender: '女',
school: '朝阳小学',
grade: 1,
class: 1
}

说起typeinterface,有一道非常经典的Typescript面试题:typeinterface的区别在哪里?

先说个人感受。我个人感觉typeinterface的区别主要是在语义上,type在官方文档的定义是类型别名,而interface的定义是接口。

下面的代码可以非常明显体现其两者在语义上的区别,其实两者在语法方面的区别并不算大。

// type可以给类型定义别名
type StudentName = string // interface可以像Java定义一个学生的抽象类
interface StudentInterface {
addRecord: (subject: string, score: number, term: string) => void
} // 等同于let name: string = '张三'
let name: StudentName = '张三' // 构造函数CollageStudent获得抽象类StudentInterface的声明
class CollageStudent implements StudentInterface { public record = [] addRecord(subject, score, term) {
this.record.push({ subject, score, term })
}
} // type其实也定义类似的类型声明结构,但是从语义上来说并不是抽象类
type TeacherType = {
subject: Array<string>
}
// 构造函数也可以获得type声明的类型,语法上是可以实现的
// 但是从语义和规范的层面上来说不推荐这么写
class CollageTeacher implements TeacherType { subject: ['数学', '体育']
}

至于标准答案,官方文档(点击此处)中给出了两者在语法上的具体区别。

泛型

什么是泛型?简单来说,泛型就是类型声明里的变量。举个不相关但是很好理解的例子:

Javascript在执行let num = 1这段代码的时候,Javascript的编译器会从右向左执行代码。代码执行之前,编译器并不知道变量num的数据类型是什么,执行完之后编译器便知道了变量num的数据类型为Number

这也正好是泛型的核心:编译之前不知道是什么类型,编译之后就知道了

// 泛型的书写形式是<T>,可以通过<T = ?>为泛型附默认值
// 函数表达式的写法
function typeOf<T>(arg: T): string {
return Object.prototype.toString.call(arg).replace(/\[object (\w+)\]/, '$1').toLowerCase()
} // 等同于typeOf<string>('Hello World')
typeOf('Hello World')
// 等同于typeOf<number>(123456)
typeOf(123456) // 函数声明式的写法
const size = <T>(args: Array<T>): number => args.length // 等同于size<number>([ 1, 2, 3 ])
size([ 1, 2, 3 ])

上述代码虽然比较简单,但是足以看出泛型的灵活性,这能让组件的复用性更高,不过可能还是不好理解泛型在实际项目中的用处。

下面是我在现实的项目工程中使用的代码片段,代码有点长但是逻辑不复杂。代码主要是用于请求后端接口的hooks,定义了两个泛型:RequestConfigAxiosResponse,分别用于定义请求参数和返回参数的结构,代码中还运用了泛型嵌套Promise<AxiosResponse<T>>,方便对多层结构的复用。

import axios, { AxiosRequestConfig } from 'axios'

// 请求参数的结构
interface RequestConfig<P> {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
data: P;
} // 返回参数的结构
interface AxiosResponse<T> {
code: number;
message?: string;
data: T;
} const $axios = axios.create({ baseURL: 'https://demo.com' }) // 声明了两个泛型类型T和P
// T - 返回参数的泛型,默认值为void,在无返回参数的时候不需要传类型声明
// P - 请求参数的泛型,默认值为void,在无请求参数的时候不需要传类型声明
// 泛型支持嵌套,如Promise<AxiosResponse<T>>即表示AxiosResponse<T>的返回值在Promise中
const useRequest = async <T = void, P = void>(requestConfig: RequestConfig<P>): Promise<AxiosResponse<T>> => { const axiosConfig: AxiosRequestConfig = { url: requestConfig.url,
data: requestConfig.data || {},
method: requestConfig.method || 'GET'
} try {
// data中是预想中的返回参数
const { data: response } = await $axios(axiosConfig) // 错误响应
if( response.code !== 200 ) { return Promise.reject(response)
} return Promise.resolve(response)
} catch(e) {
// 错误响应
return Promise.reject(e)
}
} (async () => { interface RequestInterface {
date: string;
}
interface ResponseInterface {
weather: number;
} // 无参数时使用,无需约束泛型
await useRequest({ url: 'api/connect' })
// 有参数时使用,通过泛型约束提升代码质量
const { weather } = await useRequest<RequestInterface, ResponseInterface>({
url: 'api/weather',
data: { date: '2021-02-31' }
})
})()

另外,Typescript允许类型声明调用自己,可以通过这个特性去实现类似于树形结构的需求,比较常见的就是管理系统的导航菜单了。

// Typescript支持递归调用自身
type TreeType = {
label: string;
value: string | number;
children?: Array<TreeType>
}
// 因此可以借助这个特性实现树形结构
let tree: Array<TreeType> = [ { label: '首页', value: 1, children: [
{ label: '仪表盘', value: '1-1' },
{ label: '工作台', value: '1-2' },
] },
{ label: '进度管理', value: 2, children: [
{ label: '进度设置', value: '2-1' },
{ label: '操作记录', value: '2-2' },
] },
]

新石器时代码农的Typescript开发总结的更多相关文章

  1. Typescript开发学习总结(附大量代码)

    如果评定前端在最近五年的重大突破,Typescript肯定能名列其中,重大到各大技术论坛.大厂面试都认为Typescript应当是前端的一项必会技能.作为一名消息闭塞到被同事调侃成"新石器时 ...

  2. Vue使用Typescript开发编译时提示“ERROR in ./src/main.ts Module build failed: TypeError: Cannot read property 'afterCompile' of undefined”的解决方法

    使用Typescript开发Vue,一切准备就绪.但npm start 时,提示“ ERROR in ./src/main.tsModule build failed: TypeError: Cann ...

  3. 用TypeScript开发Vue——如何通过vue实例化对象访问实际ViewModel对象

    用TypeScript开发Vue--如何通过vue实例化对象访问实际ViewModel对象 背景 我个人很喜欢TypeScript也很喜欢Vue,但在两者共同使用的时候遇到一个问题. Vue的实例化对 ...

  4. 使用Visual Studio Code搭建TypeScript开发环境

    使用Visual Studio Code搭建TypeScript开发环境 1.TypeScript是干什么的 ? TypeScript是由微软Anders Hejlsberg(安德斯·海尔斯伯格,也是 ...

  5. TypeScript开发Vue

    用TypeScript开发Vue——如何通过vue实例化对象访问实际ViewModel对象 目录 背景 解决方案 关于Vue中的计算属性类型 TypeScript的强制类型声明语法 强制类型声明的局限 ...

  6. 使用TypeScript开发ReactNative应用的简单示例

    最近小小尝试了下 ReactNative + TypeScript 开发APP,爬了无数坑之后总算弄出来个结果,重要的地方记录下,后面会附上示例代码: 1.开发工具的选择 windows 平台我接触的 ...

  7. TypeScript开发ReactNative之fetch函数的提示问题

    使用TypeScript开发ReactNative时,发现在类中调用 fetch 函数时IDE可能会提示找不到,无法加载,特别是当类中存在同名的 fetch 成员方法时更是郁闷了,虽然程序是可以执行的 ...

  8. TypeScript开发环境搭建(Visual studio code)

    使用Visual Studio Code搭建TypeScript开发环境 1.TypeScript是干什么的 ? TypeScript是由微软Anders Hejlsberg(安德斯·海尔斯伯格,也是 ...

  9. VSCode搭建node + typescript开发环境

    我们一起来喜欢TypeScript 现在写js不用TypeScript,伦家可能会觉得你是外星人. 是的,TypeScript很大程度增强了代码的可读性,可跟踪性,可维护性和减少了bug. 那么没有理 ...

随机推荐

  1. Python中“*”和“**”的用法 || yield的用法 || ‘$in’和'$nin' || python @property的含义

    一.单星号 * 采用 * 可将列表或元祖中的元素直接取出,作为随机数的上下限: import random a = [1,4] print(random.randrange(*a)) 或者for循环输 ...

  2. 7.PowerShell DSC之模式

    DSC两种模式 DSC有两种模式,Push模式和Pull模式 Push模式 基本流程 写配置--编译生成mof--推送到目标服务器,由目标服务器LCM执行mof并进行指定的配置 优点 架构简单.成本低 ...

  3. OpenStack Train版-3.安装glance镜像服务

    安装glance镜像服务 创建数据库并授权 mysql -u root create database glance; GRANT ALL PRIVILEGES ON glance.* TO 'gla ...

  4. oranges-给mini os 添加内存管理,进程多级反馈队列,进程内存完整性度量

    参考: 内存管理: https://www.jianshu.com/p/49cbaccd38c5 crc校验 https://www.cnblogs.com/zzdbullet/p/9580502.h ...

  5. leetcode 22. 括号生成 dfs

    先思考符合要求的串是什么样子的 任意时刻,(数量大于),且最后(==)==n即可 考虑下一个加入string的字符时(或者)即可 dfs class Solution { public: vector ...

  6. Python Web Framework All In One

    Python Web Framework All In One Django and Flask are the top Python web frameworks so far. Django ht ...

  7. HTTPS All In One

    HTTPS All In One HTTPS & web security HTTPS Hypertext Transfer Protocol Secure HTTPS is an exten ...

  8. Full Stack Web Development

    Full Stack Web Development Web Stacks MEAN (Mongo, Express, Angular and Node) LAMP (Linux, Apache, M ...

  9. React Hooks & Context API

    React Hooks & Context API responsive website https://reactjs.org/docs/hooks-reference.html https ...

  10. 画一个PBN大角度飞越转弯保护区

      今天出太阳了,尽管街上的行人依旧很少,但心情开始不那么沉闷了.朋友圈里除了关注疫情的最新变化之外,很多人已经开始选择读书或是和家人一起渡过这个最漫长的春节假期.陕西广电网络春节期间所有点播节目一律 ...