广告品牌营销策划蒋继平 發表於 2021-3-8 11:58:00

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

<p>如果评定前端在最近五年的重大突破,<code>Typescript</code>肯定能名列其中,重大到各大技术论坛、大厂面试都认为<code>Typescript</code>应当是前端的一项必会技能。作为一名消息闭塞到被同事调侃成“新石器时代码农”的我,也终于在2019年底上车了<code>Typescript</code>。使用的一年间整理了许多的笔记和代码片段,花了一段时间整理成了下文。</p>
<p>本文不是教程,主要目的是分享我个人在使用<code>Typescript</code>开发1年期间的一些理解和代码片段,因此文章内容主要围绕对某些特性做的研究和理解。也希望能帮到一些同在学习使用<code>Typescript</code>的小伙伴,如有错误遗漏也希望能够指出。</p>
<h3 id="基础数据类型">基础数据类型</h3>
<p><code>Javascript</code>一共有6种基础类型:<code>String</code>/<code>Number</code>/<code>Boolean</code>/<code>Null</code>/<code>Undefined</code>/<code>Symbol</code>,分别对应<code>Typescript</code>中6种类型声明:<code>string</code>/<code>number</code>/<code>boolean</code>/<code>null</code>/<code>undefined</code>/<code>symbol</code>。</p>
<p>基础数据类型的类型声明适用的几条规则:</p>
<ol>
<li><code>Typescript</code>在编译时会对代码做静态类型检查,多数情况下不支持隐式转换,即<code>let yep: boolean = 1</code>会报错</li>
<li><code>Typescript</code>中的基础类型声明的首字母不区分大小写,即<code>let num: number = 1</code>等同于<code>let num: Number = 1</code>,但是推荐小写形式</li>
<li><code>Typescript</code>允许变量有多种类型(即联合类型),通过<code>|</code>连接即可,如<code>let yep: number | boolean= 1</code>,但是不建议这么做</li>
<li>类型声明不占用变量,因此<code>let boolean: boolean = true</code>是允许的,但是不建议这么用</li>
<li>默认情况下,除了<code>never</code>,<code>Typescript</code>可以把其他类型声明(包括引用数据类型)的变量赋值为<code>null</code>/<code>undefined</code>/<code>void 0</code>而不报错。但这肯定是错误的,建议在<code>tsconfig.json</code>中设置<code>"strictNullChecks": true</code>屏蔽掉这种情况</li>
<li>对于<strong>基础类型</strong>而言,<code>unknown</code>与<code>any</code>的最终结果是一致的</li>
</ol>
<pre><code class="language-typescript">// 字符串类型声明,单引号/双引号不影响类型推断
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;
</code></pre>
<h3 id="引用数据类型">引用数据类型</h3>
<p><code>Javascript</code>的引用数据类型有很多,比如<code>Array</code>/<code>Object</code>/<code>Function</code>/<code>Date</code>/<code>Regexp</code>等,与基础类型不一样的地方是,<code>Typescript</code>有些地方并不能简单地与<code>Javascript</code>直接对应,部分的执行结果让人摸不着头脑。</p>
<p>在书写规则上,除了<code>Object</code>以外,<code>Typescript</code>其他的引用数据类型声明的首字母必须大写,如<code>let list: array&lt;number&gt; = </code>会报错,必须写成<code>let list: Array&lt;number&gt; = </code>。原因是这些引用数据类型在本质上都是构造函数,<code>Typescript</code>的底层会通过类似于<code>list instanceof Array</code>的逻辑进行类型比对。</p>
<p>其中比较有意思的一个点是:在所有的数据类型里,<code>Array</code>是唯一的泛型类型,也是唯一有两种不同的写法:<code>Array&lt;T&gt;</code>和<code>T[]</code>。</p>
<p>与数组相关的类型声明还有元组<code>Tuple</code>,跟数组的差别主要体现在:元组的长度是固定已知的。因此使用场景也非常明确,适合用在有固定的标准/参数/配置的地方,比如经纬度坐标、屏幕分辨率等。</p>
<pre><code class="language-typescript">// 数组类型有Array&lt;T&gt;和T[]两种写法
let arr1: Array&lt;number&gt; =
let arr2: number[] =

// 未开启strictNullChecks时,赋值为null/undefined/void 0不报错
let arr3: number[] = null
// 编译时不会报错,运行时报错
arr3.push(1)

// 元组类型
// 坐标表示
let coordiate: [ number, number ] =

// 其他引用数据类型
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 =&gt; alert(`Hello, ${ fullName }`)

// 当你不知道函数的返回值,但又不想用any/unknown的时候可以试试这种类型声明的写法,不过不推荐
const sayHey: Function = (fullName: string) =&gt; alert(`Hey, ${ fullName }`)
</code></pre>
<p>在<code>Typescript</code>中关于对象的类型声明一共有三种形式:<code>Object</code>/<code>object</code>/<code>{}</code>,我一开始以为<code>Object</code>会像<code>Array</code>也是泛型类型,然而经过测试发现不仅不是泛型,还有个首字母小写形式的<code>object</code>,<code>Object</code>/<code>object</code>/<code>{}</code>三者之间的执行结果完全不同。</p>
<ol>
<li>
<p>以<code>Object</code>作为类型声明时,变量值可以是任意值,如字符串/数字/数组/函数等,但是如果变量值不是对象,则无法使用其变量值特有的方法,如<code>let list: Object = []</code>不会报错,但执行<code>list.push(1)</code>会报错。造成这种情况的原因是因为在<code>Javascript</code>中,在当前对象的原型链上找不到属性/方法时,会向上一层对象进行查找,而<code>Object.prototype</code>是所有对象原型链查找的终点,也因此在<code>Typescript</code>中将类型声明成<code>Object</code>不会报错,但无法使用非对象的属性/方法</p>
</li>
<li>
<p>以<code>object</code>作为类型声明时,变量值只能是对象,其他值会报错。值得注意的是,<code>object</code>声明的对象无法访问/添加对象上的任何属性/方法,实际效果类似于通过<code>Object.create(null)</code>创建的空对象,暂时不知道这么设计的原因</p>
</li>
<li>
<p><code>{}</code>其实就是匿名形式的<code>type</code>,因此支持通过<code>&amp;</code>、<code>|</code>操作符对类型声明进行扩展(即交叉类型和联合类型)</p>
</li>
</ol>
<pre><code class="language-typescript">// 赋值给数字不会报错
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: '张三' }
</code></pre>
<h3 id="交叉类型和联合类型">交叉类型和联合类型</h3>
<p>上文提到,<code>Typescript</code>支持通过<code>&amp;</code>、<code>|</code>操作符对类型声明进行扩展,用<code>&amp;</code>相连的多个类型是交叉类型,用<code>|</code>相连的多个类型是联合类型。</p>
<p>两者之间的区别主要体现在联合类型主要在做类型的合并,如<code>Form4Type</code>、<code>Form6Type</code>;而交叉类型则是求同排斥,如<code>Form3Type</code>、<code>Form5Type</code>。也可以用数学上的合集和并集来分别理解联合类型和交叉类型。</p>
<pre><code class="language-typescript">
type Form1Type = { name: string; } &amp; { 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; } &amp; { 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; } &amp; { 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 } // 验证通过
</code></pre>
<p>上述的代码片段一般只会在面试题里面出现,如果这种代码出现在真实的项目代码里面,估计在代码评审的时候就直接被点名批评了。</p>
<p>不过也不是没有实用场景,以苹果的教育优惠举个例子:假设原价购买苹果12需要5000元;如果通过教育优惠购买则可以享受一定折扣的优惠(比如打8折),但是需要提供学生证或者是教师证。经过产品经理的整理,转变为需求文档之后可能就变成了:原价购买无需其他材料,如需享受教育优惠,则需要提交个人资料以及学生证/教师证扫描件。</p>
<pre><code class="language-typescript">// 原价购买
type StandardPricing = {
mode: 'standard';
}
// 教育优惠购买需要提供购买人姓名和相关证件
type EducationPricing = {
mode: 'education';
buyer_name: string;
sic_or_tic: string;
}
// 通过&amp;和|合并类型
type buyiPhone12 = { price: number; } &amp; ( StandardPricing | EducationPricing )


let standard: buyiPhone12 = { mode: 'standard', price: 5000 }
let education: buyiPhone12 = { mode: 'education', price: 4000, buyer_name: '张三', sic_or_tic: '证件' }
</code></pre>
<h3 id="type和interface">Type和Interface</h3>
<p>在一开始学习<code>Typescript</code>的时候看到<code>interface</code>,我第一时间想到的是<code>Java</code>。<code>Java</code>的<code>interface</code>是一种抽象类,把功能的定义和具体的实现进行分离,方便不同人员可以通过<code>interface</code>进行相互配合,类似于需求文档在开发中的作用。</p>
<pre><code class="language-java">// 张三定义了用户中心的功能有三个:登录、注册、找回密码
interface UserCenterDao {
void userLogin();
void userRegister();
void userResetPassword();
}

// 李四开发用户中心的功能就会提示需要实现三个功能
class UserCenter implements UserCenterDao {
public void userLogin() {};
public void userRegister() {};
public void userResetPassword() {};
}
</code></pre>
<p><code>Typescript</code>对于<code>interface</code>的定义也是类似,都是声明一系列的抽象变量/方法,然后通过具体的代码去实现。</p>
<p><code>interface</code>整体的效果与用<code>type</code>声明的效果非常相似,即使是专属于<code>interface</code>的继承<code>extends</code>,<code>type</code>也可以通过<code>&amp;</code>、<code>|</code>操作符实现,两者之间也不是独立的,也可以互相进行调用。</p>
<p>因此在平时的实际开发中,不必太过纠结使用<code>type</code>还是<code>interface</code>进行类型的声明,特别纠结的时候<code>type</code>一把梭。</p>
<pre><code class="language-typescript">// 用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
}
</code></pre>
<p>说起<code>type</code>和<code>interface</code>,有一道非常经典的<code>Typescript</code>面试题:<code>type</code>和<code>interface</code>的区别在哪里?</p>
<p>先说个人感受。我个人感觉<code>type</code>和<code>interface</code>的区别主要是在语义上,<code>type</code>在官方文档的定义是类型别名,而<code>interface</code>的定义是接口。</p>
<p>下面的代码可以非常明显体现其两者<strong>在语义上的区别</strong>,其实两者在语法方面的区别并不算大。</p>
<pre><code class="language-typescript">// type可以给类型定义别名
type StudentName = string

// interface可以像Java定义一个学生的抽象类
interface StudentInterface {
addRecord: (subject: string, score: number, term: string) =&gt; 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&lt;string&gt;
}
// 构造函数也可以获得type声明的类型,语法上是可以实现的
// 但是从语义和规范的层面上来说不推荐这么写
class CollageTeacher implements TeacherType {

subject: ['数学', '体育']
}
</code></pre>
<p>至于标准答案,官方文档(点击此处)中给出了两者在语法上的具体区别。</p>
<p><img src="https://img2020.cnblogs.com/blog/2113379/202103/2113379-20210306174113319-283064632.png"></p>
<h3 id="泛型">泛型</h3>
<p>什么是泛型?简单来说,<strong>泛型就是类型声明里的变量</strong>。举个不相关但是很好理解的例子:</p>
<p><code>Javascript</code>在执行<code>let num = 1</code>这段代码的时候,<code>Javascript</code>的编译器会从右向左执行代码。代码执行之前,编译器并不知道变量<code>num</code>的数据类型是什么,执行完之后编译器便知道了变量<code>num</code>的数据类型为<code>Number</code>。</p>
<p>这也正好是泛型的核心:<strong>编译之前不知道是什么类型,编译之后就知道了</strong>。</p>
<pre><code class="language-typescript">// 泛型的书写形式是&lt;T&gt;,可以通过&lt;T = ?&gt;为泛型附默认值
// 函数表达式的写法
function typeOf&lt;T&gt;(arg: T): string {
    return Object.prototype.toString.call(arg).replace(/\/, '$1').toLowerCase()
}

// 等同于typeOf&lt;string&gt;('Hello World')
typeOf('Hello World')
// 等同于typeOf&lt;number&gt;(123456)
typeOf(123456)

// 函数声明式的写法
const size = &lt;T&gt;(args: Array&lt;T&gt;): number =&gt; args.length

// 等同于size&lt;number&gt;([ 1, 2, 3 ])
size([ 1, 2, 3 ])
</code></pre>
<p>上述代码虽然比较简单,但是足以看出泛型的灵活性,这能让组件的复用性更高,不过可能还是不好理解泛型在实际项目中的用处。</p>
<p>下面是我在现实的项目工程中使用的代码片段,代码有点长但是逻辑不复杂。代码主要是用于请求后端接口的hooks,定义了两个泛型:<code>RequestConfig</code>和<code>AxiosResponse</code>,分别用于定义请求参数和返回参数的结构,代码中还运用了泛型嵌套<code>Promise&lt;AxiosResponse&lt;T&gt;&gt;</code>,方便对多层结构的复用。</p>
<pre><code class="language-typescript">import axios, { AxiosRequestConfig } from 'axios'

// 请求参数的结构
interface RequestConfig&lt;P&gt; {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
data: P;
}

// 返回参数的结构
interface AxiosResponse&lt;T&gt; {
code: number;
message?: string;
data: T;
}

const $axios = axios.create({ baseURL: 'https://demo.com' })

// 声明了两个泛型类型T和P
// T - 返回参数的泛型,默认值为void,在无返回参数的时候不需要传类型声明
// P - 请求参数的泛型,默认值为void,在无请求参数的时候不需要传类型声明
// 泛型支持嵌套,如Promise&lt;AxiosResponse&lt;T&gt;&gt;即表示AxiosResponse&lt;T&gt;的返回值在Promise中
const useRequest = async &lt;T = void, P = void&gt;(requestConfig: RequestConfig&lt;P&gt;): Promise&lt;AxiosResponse&lt;T&gt;&gt; =&gt; {

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 () =&gt; {

interface RequestInterface {
    date: string;
}
interface ResponseInterface {
    weather: number;
}

// 无参数时使用,无需约束泛型
await useRequest({ url: 'api/connect' })
// 有参数时使用,通过泛型约束提升代码质量
const { weather } = await useRequest&lt;RequestInterface, ResponseInterface&gt;({
    url: 'api/weather',
    data: { date: '2021-02-31' }
})
})()
</code></pre>
<p>另外,<code>Typescript</code>允许类型声明调用自己,可以通过这个特性去实现类似于树形结构的需求,比较常见的就是管理系统的导航菜单了。</p>
<pre><code class="language-typescript">// Typescript支持递归调用自身
type TreeType = {
label: string;
value: string | number;
children?: Array&lt;TreeType&gt;
}
// 因此可以借助这个特性实现树形结构
let tree: Array&lt;TreeType&gt; = [

{ label: '首页', value: 1, children: [
    { label: '仪表盘', value: '1-1' },
    { label: '工作台', value: '1-2' },
] },
{ label: '进度管理', value: 2, children: [
    { label: '进度设置', value: '2-1' },
    { label: '操作记录', value: '2-2' },
] },
]
</code></pre><br><br>
来源:https://www.cnblogs.com/fei-hui/p/14498919.html
頁: [1]
查看完整版本: Typescript开发学习总结(附大量代码)