不认命的奥德彪 發表於 2022-3-2 19:41:00

快速上手typescript(基础篇)

<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202203/1213309-20220302193723317-1317460915.jpg"></div>
<h1 id="壹--引">壹 ❀ 引</h1>
<p>在<code>javascript</code>开发中,你可能也遇到过我这样的苦恼,在维护某段几年前的老旧代码时,我发现了某个数据加工方法<code>fn</code>,而且根据现有逻辑来看<code>fn</code>的某个参数是一个数组,因为新需求我需要对数组做一次过滤,于是我在代码中补了一段<code>params.filter</code>类似的代码,测试没问题顺利上线,然后第二天客户反馈页面白屏直接功能直接炸了,打开控制台一看,不能从<code>undefined</code>上读取数组方法<code>filter</code>,这个参数在某个不为人知的场景下根本就没传递,整个人就大无语。</p>
<pre><code class="language-javascript">const fn = (a, b) =&gt; {
const b_ = b.filter();
}
</code></pre>
<p>你会发现在某些场景下,我们根本无法得知某个变量是不是一定就是这个类型,某个变量是不是一定会按你预期进行传递。那有没有什么办法在我们书写代码时就能提前检测,而不是只有代码跑起来才能感知呢(让客户做测试都会死的很明白)?这时候我们就需要<code>typescript</code>了。</p>
<p>与传统动态弱类型语言<code>javascript</code>不同,静态类型的<code>typescript</code>在执行前会先编译成<code>javascript</code>,因为它强大的<code>type</code>类型系统加持,能让我们在编写代码时增加更多严谨的限制。注意,它并不是一门全新的语言,所以并没有增加额外的学习成本,你甚至可以将它理解为新增了类型封装的<code>javascript</code>,因此原先代码逻辑怎么写现在还是一样的写,至于底层编译的事我们无需关心。</p>
<p>本文将作为<code>typescript</code>的基础篇,当然本质上也是我自己的知识梳理笔记,本文中所有例子都可以在typescript练习上在线编辑,方便某些还不想在项目中安装<code>typescript</code>的同学,那么本文开始。</p>
<h1 id="贰--原始数据类型">贰 ❀ 原始数据类型</h1>
<p><code>typescript</code>与<code>js</code>一样也有原始数据类型与对象数据类型,我们先来介绍常用的原始数据类型。</p>
<h4 id="贰--壹-string">贰 ❀ 壹 string</h4>
<pre><code class="language-typescript">let str: string = '听风是风';
// 支持模板字符串
const echo = '听风是风';
let s: string = `name is ${echo}`;
</code></pre>
<h4 id="贰--贰-number">贰 ❀ 贰 number</h4>
<pre><code class="language-typescript">let num: number = 1;
let notANumber: number = NaN;
</code></pre>
<h4 id="贰--叁-boolean">贰 ❀ 叁 boolean</h4>
<pre><code class="language-typescript">let bool: boolean = true;
</code></pre>
<h4 id="贰--肆-任意类型any">贰 ❀ 肆 任意类型any</h4>
<p><code>any</code>用于表示任意类型,如果一个变量的类型为<code>any</code>,那么它可以被赋予任意类型的值,你可能会觉得<code>any</code>应该没啥使用场景,但恰恰相反的是,在日常赶项目赶交付期间,研发同学可能没有太多时间去做细致化类型定义,于是<code>any</code>走天下,这也是<code>typescript</code>又被戏称为<code>anyscript</code>的原因。</p>
<pre><code class="language-typescript">let a: any = 4;
a = "4";
a = false;
</code></pre>
<p>这一点也能证明<code>typescript</code>所有类型都属于<code>any</code>的子类型。</p>
<h4 id="贰---伍-undefined与null">贰 ❀伍 undefined与null</h4>
<pre><code class="language-typescript">let a: undefined = undefiend;
let b: null = null;
</code></pre>
<p>默认情况下<code>undefined</code>与<code>null</code>这两个类型属于所有类型的子类型,也就是说你能将一个<code>undefined</code>复制给一个<code>number</code>的变量,但是开发中一般不推荐这么做,既然我们希望<code>typescript</code>为我们添加严格的类型判断,那么严格尊重总是有好处,你肯定也希望某种情况下<code>undefiened</code>调用了某个<code>number</code>的<code>API</code>。</p>
<p>我们可以在<code>tsconfig</code>配置中添加<code>strictNullChecks:true</code>的开关,来启用严格的控制判断,这样<code>undefined</code>或者<code>null</code>就只能赋值给它们自身类型的变量,虽然这两个类型本身用的也不多。</p>
<h4 id="贰---陆-空-void">贰 ❀陆 空 void</h4>
<p><code>void</code>与<code>any</code>相反,它表示没有任意类型,比如一个函数需要返回一个字符串,你可以定义函数返回值的类型:</p>
<pre><code class="language-typescript">const fn =(): string =&gt; {
    return '1';
}
</code></pre>
<p>但假设这个函数没有返回值,那么就可以用<code>void</code>:</p>
<pre><code class="language-typescript">const fn =(): void =&gt; {
    console.log(1);
}
</code></pre>
<p>但事实上开发中如果一个函数无返回,我们也不会写<code>void</code>,因为本身也没什么意义,所以<code>void</code>使用并不多。</p>
<p>另外即使打开了<code>strictNullChecks</code>开关,我们还是能将<code>undefined</code>与<code>null</code>赋予给<code>void</code>类型的变量,即使这也没啥意义= =。</p>
<h1 id="叁--数组类型">叁 ❀ 数组类型</h1>
<p><code>array</code>定义支持两种写法,一种是<code>类型[]</code>,另一种是<code>Array&lt;类型&gt;</code>,比如:</p>
<pre><code class="language-typescript">// 元素全是number类型的数组
let arr1: number[] = ;
// 元素全是string类型的数组
let arr2: Array&lt;string&gt; = ['1', '2', '3'];
</code></pre>
<p><code>number[]</code>就表示这个数组的所有元素都必须是数字,包括之后你也不能往数组中添加非数字的元素,比如:</p>
<pre><code class="language-typescript">let arr: number[] = ;
arr.push('听风是风');// error 听风是风不是number类型
</code></pre>
<p>那假设数组元素种类比较多,我们可以用<code>any</code>来表示数组中可以出现任意类型,比如:</p>
<pre><code class="language-typescript">let arr: any[] = ;
arr.push(undefined);
</code></pre>
<h1 id="肆--接口类型对象类型">肆 ❀ 接口类型(对象类型)</h1>
<p>我们一般用接口<code>interface</code>来描述对象类型(这里的对象指<code>{}</code>),比如人都有名字,性别年龄等属性,接口即可用于对人这个类的形状进行抽象描述,直接看个例子:</p>
<pre><code class="language-typescript">// 我们定义了一个叫Person的接口,推荐首字母大写
interface Person {
    name: string;
    age: number;
}

let echo: Person = {
    name: '听风是风',
    age: 28,
}
</code></pre>
<p>我们定义了一个接口<code>Person</code>,然后变量<code>echo</code>使用了接口<code>Person</code>,可以看到<code>echo</code>和<code>Person</code>的属性形状需要保持一致,缺属性或者多属性都不行:</p>
<pre><code class="language-typescript">// 少属性
// error Property 'age' is missing in type '{ name: string; }'.
let echo: Person = {
    name: '听风是风',
}
// 多属性
let echo: Person = {
    name: '听风是风',
    age: 28,
    gender: 'male'// Person上未指定gender:string
}
</code></pre>
<h4 id="肆--壹-可选属性">肆 ❀ 壹 可选属性</h4>
<p>接口支持使用<code>?</code>让某个属性变为可选,女性的年龄都是秘密,某种场景下我们并不能拿到<code>age</code>属性,那么我们可以让<code>Person</code>的<code>age</code>为可选,比如:</p>
<pre><code class="language-typescript">interface Person {
    name: string;
    age?: number;
}

// 缺少了age属性,但并不会报错
let echo: Person = {
    name: '西西',
}
</code></pre>
<h4 id="肆--贰-只读属性">肆 ❀ 贰 只读属性</h4>
<p>除了限定属性可选,我们还可以通过<code>readonly</code>字段来限制某个属性只读,比如:</p>
<pre><code class="language-typescript">interface Person {
    readonly name: string;
    age: number;
}

let echo: Person = {
    name: '听风是风',
    age: 28,
}
echo.name = '时间跳跃';// error 无法修改只读属性
</code></pre>
<p>可以看到<code>name</code>添加了只读,因此它在初始化之后就无法修改。</p>
<h4 id="肆--叁-限制接口属性范围">肆 ❀ 叁 限制接口属性范围</h4>
<p>比如上述代码我们限制了名称是字符串,也就是说任意字符串都可以,假设我们设计了一个组件,它的名称属性将决定组件如何展示,而且我们预定了只支持两种模式,那么此时我们就可以更精确的来限制属性范围,比如:</p>
<pre><code class="language-typescript">interface P {
    name: 'wide'| 'narrow';
}

let props:P = {
name: 'wide'
}
</code></pre>
<p>此时<code>props</code>的<code>name</code>字段只能是<code>wide</code>或者<code>narrow</code>其一,输入其它就会报错,这样能很好的让组件属性输入符合预期。</p>
<h4 id="肆--肆-额外的任意属性">肆 ❀ 肆 额外的任意属性</h4>
<p>某些情况我们希望接口能添加任意属性,那么可以通过如下方式:</p>
<pre><code class="language-typescript">interface Person {
    name: string;
    age?: number;
    : any;
}

let echo: Person = {
    name: '听风是风',
    age: 28,
    hobby: '吃西瓜',
    gender: 'male'
}
</code></pre>
<p><code>: any</code>表示可以添加变量名为<code>string</code>且值为<code>any</code>类型,<code>propName</code>的类型只会限制额外的属性,假设我们将其改为<code></code>,那么<code>hobby</code>与<code>gender</code>就会报错,而我们将其变量名改为数字则不会有问题:</p>
<pre><code class="language-typescript">interface Person {
    name: string;
    age?: number;
    : any;
}

let echo: Person = {
    name: '听风是风',
    age: 28,
    1: '吃西瓜',
    2: 'male'
}
</code></pre>
<p>但重点需要注意的是,假设我们定了额外的属性,那么确定属性以及可选属性的类型一定得是额外属性的子类型,比如上面<code>string</code>和<code>number</code>都是<code>any</code>的子类型,假设我们将<code>any</code>改为<code>string</code>,那么<code>age</code>属性就会报错:</p>
<pre><code class="language-typescript">interface Person {
    name: string;
    // 类型“number”的属性“age”不能赋给字符串索引类型“string”
    age?: number;
    : string;
}

let echo: Person = {
    name: '听风是风',
    age: 28,
    hobby: '吃西瓜',
    gender: 'male'
}
</code></pre>
<p>假设我们不想定义<code>any</code>来包含<code>string</code>与<code>number</code>,这里也可以使用联合类型<code>string | number</code>来取代<code>any</code>:</p>
<pre><code class="language-typescript">interface Person {
    name: string;
    age?: number;
    : string | number;
}

let echo: Person = {
    name: '听风是风',
    age: 28,
    hobby: '吃西瓜',
    gender: 'male'
}
</code></pre>
<h1 id="伍--函数类型">伍 ❀ 函数类型</h1>
<p><code>javascript</code>中创建函数常用有函数声明与函数表达式两种形式,由于函数有输入和输出,所以我们需要对参数以及返回结果都做限制,我们来分别介绍两种写法。</p>
<h4 id="伍--壹-函数声明">伍 ❀ 壹 函数声明</h4>
<pre><code class="language-typescript">function sum(x: number, y: number): number {
    return x + y;
};
</code></pre>
<p>比如上述的<code>sum</code>函数就接受2个类型为数字的变量<code>x,y</code>,并会返回它们的和,和的类型也是数字。</p>
<p>在限制下我们调用函数时少传多传,或者传递的参数类型不对都会有错误提示:</p>
<pre><code class="language-typescript">sum(1,'2');// error '2'的不是数字类型
sum(1);// error 需要2个参数但只传递了1个
sum(1,2,3)// error 需要2个参数但传递了3个
</code></pre>
<h4 id="伍--贰-函数表达式">伍 ❀ 贰 函数表达式</h4>
<p>我们将上面的代码改为函数表达式可以是这样:</p>
<pre><code class="language-typescript">let sum = function (x: number, y: number): number {
    return x + y;
};
</code></pre>
<p>但其实这种写法是省略了<code>sum</code>类型的写法,让<code>typescript</code>自己进行了类型推断,啥意思呢?我们将鼠标放到<code>sum</code>上你就能看到<code>sum</code>自身的类型限制:</p>
<div align="center"><img src="https://img2022.cnblogs.com/blog/1213309/202203/1213309-20220302193651758-997254676.png"></div>
<p>意思就是,我们将一个匿名函数赋值给了<code>sum</code>,匿名函数虽然做了参数以及返回值的类型限定,但是我们没对变量<code>sum</code>做类型限定,<code>sum</code>是什么类型?很显然是函数类型,所以完整的写法应该是这样:</p>
<pre><code class="language-typescript">let sum: (x: number, y: number) =&gt; number = function (x: number, y: number): number {
return x + y;
};
</code></pre>
<p>注意<code>(x: number, y: number) =&gt; number</code>这一段是对于<code>sum</code>这个变量类型的描述,表示它是一个函数类型,接受了哪些参数,分别是什么类型,以及返回什么类型。正常我们在接口中描述对象某个属性是一个函数也是相同的写法,比如:</p>
<pre><code class="language-typescript">interface Person {
    name: string;
    age: number;
    canFly: () =&gt; boolean
};

let echo: Person = {
    name: '听风是风',
    age: 28,
    canFly: function () { return false }
}
</code></pre>
<p>在接口中我们对<code>canFly</code>进行了描述,它是一个函数类型,没有入参且返回一个布尔值,于是在变量<code>echo</code>中我们具体实现了这个方法,同样没有入参,直接返回一个布尔值。这就相当于函数类型说明都被提到接口中统一描述了,而到具体实现时你不用重复再限制一次。而在上面的函数表达式中,我们要么省略变量的限制让<code>typescript</code>自行推断,要么我们手动补全变量的函数类型限制,其实就是这个意思。</p>
<p>当然,如果你觉得自己补全看着函数太长了,我们也能将函数变量这一块的描述交给接口,这样看着就相对简洁一点:</p>
<pre><code class="language-typescript">// 将函数变量的约束抽离出来给接口来做
interface MySum {
    (x: number, y: number): number
}

let sum: MySum = function (x: number, y: number): number {
    return x + y;
};
</code></pre>
<h4 id="伍--叁-可选参数">伍 ❀ 叁 可选参数</h4>
<p>函数同样支持使用<code>?</code>来表示某个参数可选:</p>
<pre><code class="language-typescript">function sum(x: number, y?: number): number {
    return x + y;
};

sum(1);
</code></pre>
<h4 id="伍--肆-参数默认值">伍 ❀ 肆 参数默认值</h4>
<p>有默认值的参数在<code>typescript</code>中会被默认识别为可选参数,毕竟有默认值传不传递都可以:</p>
<pre><code class="language-typescript">function sum(x: number, y: number = 1): number {
    return x + y;
};

sum(1);
</code></pre>
<h4 id="伍--伍-rest参数">伍 ❀ 伍 ...rest参数</h4>
<p>在<code>es6</code>中我们可以用<code>...rest</code>来表示函数剩余参数,这也巧妙解决了<code>arguments</code>是类数组无法使用数组<code>api</code>的问题,因为在函数内<code>rest</code>就是一个数组,因此我们可以用数组类型来描述<code>rest</code>,比如:</p>
<pre><code class="language-typescript">function fn(a: number, ...rest: any[]) {
    rest.forEach((item) =&gt; console.log(item))
}
fn(1, 2, 3, 4, 5);
</code></pre>
<h1 id="陆--联合类型">陆 ❀ 联合类型</h1>
<p>很多时候,我们可能需要让一个变量支持数字以及字符串等多种类型,这时候就需要使用联合类型,它使用符号<code>|</code>表示,比如:</p>
<pre><code class="language-typescript">// echo的值可以是数字或者字符串
let echo: number | string = 'echo';
echo = 1;
</code></pre>
<p>以上代码中的<code>echo</code>可以被赋值为任意的字符串或数字类型的值。但需要注意的是,当我们未给<code>echo</code>赋予准确的值,但需要访问某个属性或<code>api</code>,此时只能访问联合类型共有的属性或者方法,比如:</p>
<pre><code class="language-typescript">let echo: number | string;
// 数字和字符串都支持toString方法
echo.toString();
// 报错,Property 'toFixed' does not exist on type 'string'.
echo.toFixed(1);
</code></pre>
<p>但假设我们给<code>echo</code>赋予具体的值,此时联合类型同样会走类型推断,从而让我们能正确使用对应类型的<code>api</code>,比如:</p>
<pre><code class="language-typescript">let echo: number | string;
// 此时被推断成字符串,因此能调用字符串的api
echo = '听风是风';
echo.length;// 4

// 此时被推断成数字,因此能调用数字的api
echo = 1.021;
echo.toFixed(2); // '1.02'
</code></pre>
<h1 id="柒--类型推断">柒 ❀ 类型推断</h1>
<p>首先<strong>类型推断</strong>属于<code>typescript</code>的一个概念,相当于<code>typescript</code>底层自动会帮我们做的一件事,大家作为了解就好。</p>
<p>正常来说我们定义字符串是这样,我们明确标明了<code>str</code>的类型,以及符合预期的修改<code>str</code>的值:</p>
<pre><code class="language-typescript">let str: string = 'echo';
str = '听风是风';
</code></pre>
<p>但假设我们不去定义一个变量的类型,但赋予了这个变量一个明确的值,比如一个字符串,再修改值为数字时,你会发现报错了:</p>
<pre><code class="language-typescript">let str = 'echo';
str = 1; //Type '1' is not assignable to type 'string'.
</code></pre>
<p>这是因为<code>typescript</code>会根据我们最初赋予的值,尝试去推断这个变量的类型,所以即便我们没指定具体类型,后续也不能随意修改值的类型,这就是所谓的<strong>类型推断</strong>了。</p>
<p>上述代码等价于:</p>
<pre><code class="language-typescript">let str: string = 'echo';
str = 1; //Type '1' is not assignable to type 'string'.
</code></pre>
<p>也就是说,只要你定义的文件是<code>.ts</code>,就别想着在<code>ts</code>文件不定义类型然后随意赋值,这对于<code>ts</code>而言肯定是不允许的。除了上文提到的几个不常用的类型,日常开发中我们还是希望能明确标明变量类型。</p>
<p>还有一种比较特殊,我指定以了一个变量但没赋值,这时候因为没具体的值,所以<code>typescript</code>会将这个变量推断成<code>any</code>类型,因此我们可以随意修改这个变量的值,比如:</p>
<pre><code class="language-typescript">let echo;
echo = '时间跳跃';
echo = 1;

// 等同于
let echo: any;
echo = '时间跳跃';
echo = 1;
</code></pre>
<h1 id="捌--类型断言">捌 ❀ 类型断言</h1>
<p>如果说类型推断是<code>typescript</code>自动做的类型判断,那么类型断言就是我们人为手动的来指定一个值的类型,它支持两种写法:</p>
<pre><code class="language-typescript">// &lt;类型&gt;变量名
&lt;string&gt;echo
// 变量名 as 类型
echo as string
</code></pre>
<p>需要注意的是,在<code>tsx</code>文件中只支持<code>as</code>这种写法,保险起见统一使用<code>as</code>更稳。</p>
<p>为什么会有类型断言的使用场景?我们来看几个例子你就明白了。</p>
<h4 id="捌--壹-联合类型断言场景">捌 ❀ 壹 联合类型断言场景</h4>
<p>我们前面说了,当一个变量没具体赋值,且有联合类型时,它只能使用联合类型共有的属性或方法,那假设我们现在封装了一个方法:</p>
<pre><code class="language-typescript">function fn(s: string | number): number {
    // 报错 Property 'length' does not exist on type 'number'.
    return s.length;
};
</code></pre>
<p>我们假定参数<code>s</code>可能是字符串或者数字两种类型,但是你很清楚这个方法只会接受到字符串,那我们就可以手动指定<code>s</code>的类型,比如:</p>
<pre><code class="language-typescript">function fn(s: string | number): number {
    return (s as string).length;
};
</code></pre>
<p>类型断言的目的就是我们开发者主动的告诉<code>typescipt</code>,我现在很清楚这个变量此时的类型是什么,从而让<code>typescript</code>不报错,但上述编码编译成<code>js</code>后其实也只是一个普通的返回<code>s.length</code>的函数,假设参数依旧传递了一个数字进来,那么还是无法避免报错的尴尬,所以使用类型断言时一定得谨慎,它只是绕过<code>typescirpt</code>报错,并没有从根源上解决代码兼容问题。</p>
<p>上述代码通过<code>if</code>来限制执行,你会发现这样实现其实更稳:</p>
<pre><code class="language-typescript">function fn(s: string | number): number {
    let length = 0;
    // 我们添加了判断,也相当于了人为对类型做了判断,因此也不会报错
    if (typeof s === 'string') {
      length = s.length;
    };
    return length;
};
</code></pre>
<h4 id="捌--贰-父子类断言场景">捌 ❀ 贰 父子类断言场景</h4>
<p><code>ES6</code>支持类的定义与继承,而当类具有继承关系时,类型断言也会起到作用,比如:</p>
<pre><code class="language-typescript">class P { }

class P1 extends P {
    a: number = 1;
}
class P2 extends P {
    b: number = 2;
}

const fn = (s: P): boolean =&gt; {
    // 报错 Property 'a' does not exist on type 'P'.
    if (typeof s.a === 'number') {
      return true;
    }
    return false;
}
</code></pre>
<p>这里我们定义了一个父类<code>P</code>,基于<code>P</code>继承得到了<code>P1 P2</code>两个类,且两个类都有属于自己的实例属性,现在我们定义了一个比较通用的检测<code>P</code>类属性的方法,考虑到公用型,所以类型定义我们使用<code>P</code>,但在内部实现中,<code>typescript</code>会告诉你<code>P</code>上并没有属性<code>a</code>。这时候我们同样可以利用断言将类型精确到子类<code>P1</code>,如下:</p>
<pre><code class="language-typescript">const fn = (s: P): boolean =&gt; {
    if (typeof (s as P1).a === 'number') {
      return true;
    }
    return false;
}
</code></pre>
<p>当然这也只是绕过了<code>typescript</code>的检测,假设我们传递了一个<code>P2</code>实例进来,你会发现代码会报错,这并没有解决根本问题。所以更好的做法还是从逻辑层间提升代码稳定性,比如:</p>
<pre><code class="language-typescript">const fn = (s: P): boolean =&gt; {
    if (s instanceof P1) {
      return true;
    }
    return false;
}
</code></pre>
<h4 id="捌--叁-将任意类型断言为any">捌 ❀ 叁 将任意类型断言为any</h4>
<p><code>javascript</code>中存在很多原生对象,这些对象一开始就没被添加类型,比如我们希望在全局对象<code>window</code>上添加属性就会报错:</p>
<pre><code class="language-typescript">window.echo = 1;// error window上不存在echo的类型
</code></pre>
<p>这时候我们可以手动将<code>window</code>断言为<code>any</code>以解决修改属性以及添加属性的问题:</p>
<pre><code class="language-typescript">(window as any).echo = 1;
</code></pre>
<p>我们知道<code>window</code>上默认自带一些属性,比如<code>name</code>字段默认是一个空字符,因此在<code>typescript</code>中<code>name</code>也默认被推断成了<code>string</code>类型,比如我们想将<code>window.name</code>修改为数字默认会报错:</p>
<pre><code class="language-typescript">window.name = 1;// error 不能将1赋予给字符串类型的name
</code></pre>
<p>而断言成<code>any</code>可以让我们任意修改<code>window</code>上的属性类型,这很方便但也有一定风险,在实际开发中请谨慎对待。</p>
<h4 id="捌--肆-将any断言成精确的类型">捌 ❀ 肆 将any断言成精确的类型</h4>
<p>在旧有代码迁移<code>ts</code>或者维护不规范的<code>ts</code>代码时,我们可能会遇到因为赶项目赶时间而定义比较随意的<code>any</code>类型,而你了解了这段代码其实知道类型定义可以更为精确,重写重构代码写出完全规范的代码之外,你能通过断言对于模糊类型进行补救,比如:</p>
<pre><code class="language-typescript">interface UserInfo {
    name: string;
    age: number;
}

function getUserInfo(): any {
    return {
      name: '听风是风',
      age:28
    }
};

const user = getUserInfo() as UserInfo;
</code></pre>
<p>这样<code>user</code>后续的代码就能清楚知道这个对象是什么类型,对应让代码编写更严谨。</p>
<h4 id="捌--伍-类型断言的限制">捌 ❀ 伍 类型断言的限制</h4>
<p>断言虽然在某些场景很好用,但它也得满足一些断言场景,毕竟我们总不能将猫的类型断言成鱼的类型,这就不符合规范了。那么满足什么条件才能断言呢?先说结论,当<strong>类型A兼容了类型B,或者B兼容了A,那么A可以断言成B,B也能断言成A</strong>。</p>
<p>什么意思?我们在上文提到,所有的类型都是<code>any</code>的子类型,也就是说<code>any</code>兼容了其它所有类型,比如<code>string</code>类型,因此我们可以将<code>any as string</code>,也能将<code>string as any</code>,这都是可以的。</p>
<p>我们再来看个例子:</p>
<pre><code class="language-typescript">interface Person {
    name: string;
}
interface User {
    name: string;
    age: number;
}

let echo: User = {
    name: 'Tom',
    age: 28
};
// 注意,echo包含age,但Person类并没有age
let xixi: Person = echo;
</code></pre>
<p>我们定义了<code>Person</code>与<code>User</code>两个接口,比较有趣的事,我们将已定义了<code>User</code>类的变量<code>echo</code>赋值给<code>xixi</code>,而<code>xixi</code>的类<code>Person</code>并没有<code>age</code>属性,但并不会报错,而假设我们将代码改为如下这样,就会报错:</p>
<pre><code class="language-typescript">interface Person {
    name: string;
}
interface User {
    name: string;
    age: number;
}

let echo: User = {
    name: 'Tom',
    age: 28
};
let animal: Person = {
    name: 'Tom',
    age: 28 //error age在Person中未定义
};
</code></pre>
<p>为啥上面正常,下面这段代码就报错了,区别在哪?区别就在于上面的代码的<code>echo</code>提前定义好了<code>User</code>类型,而下面的赋值只是一个单纯的对象,是一个数据,它无类型。</p>
<p>那为什么上面不报错呢?其实说到底还是底层类型断言帮我们做了处理,上面的<code>Person</code>与<code>User</code>类型的关系你可以理解为:</p>
<pre><code class="language-typescript">interface Person {
    name: string;
}

// User继承了Person接口,并额外添加了age
interface User extends Person{
    age: number;
}
</code></pre>
<p>因此接口<code>Person</code>兼容了<code>User</code>(有相同属性,属性少的兼容属性多的)。还记得上文父子类断言场景中,我们将父类断言成子类的操作吗?你没发现参数处我们用了属性更少的父类<code>P</code>,但事实上传递进来的参数可能是接口<code>P1</code>或者<code>P2</code>的对象,它们的属性都比<code>P</code>接口定义的属性要多,但是参数这里并不会报错,而且在函数内我们还能将<code>P</code>断言为<code>P1</code>,本质原因也是<code>P</code>兼容了<code>P1 P2</code>。</p>
<p>其实总结上面聊到的几个使用场景:</p>
<ul>
<li>联合类型<code>string | number</code>断言为<code>string</code>,两者存在兼容关系。</li>
<li>父类断言成子类场景,两者同样存在兼容关系。</li>
<li><code>any</code>断言成任意,任意类型断言成<code>any</code>,本质上也是兼容关系。</li>
</ul>
<p>因此,只要两个类型存在兼容关系,不管谁兼容谁,都能相互断言成对方的类型。</p>
<p>另外,我们在前面说,猫类型不能断言成鱼类型,但现在你会发现一个很有趣的事情,猫类型和<code>any</code>是包含关系,而<code>any</code>和鱼类型同样是包含关系,那我能不能<code>cat as any as fish</code>双重断言直接实现跨物种进化呢?很明显通过这种做法,我们能实现类的任意断言,但<code>typescript</code>中一般不推荐这么做,因为大概率会导致类型错误....</p>
<h4 id="捌--伍-类型断言与类型声明">捌 ❀ 伍 类型断言与类型声明</h4>
<p>我们在将<code>any</code>断言成其它类型讲解中,为了完善类型定义模糊的代码,我们给了一个例子,其实它还有其它的修复方法,比如不全函数的返回值的类型:</p>
<pre><code class="language-typescript">interface UserInfo {
    name: string;
    age: number;
}

function getUserInfo(): UserInfo {
    return {
      name: '听风是风',
      age:28
    }
};

const user = getUserInfo();
</code></pre>
<p>除此之外,我们还能补全<code>user</code>的类型声明,达到相同的效果,比如:</p>
<pre><code class="language-typescript">interface UserInfo {
    name: string;
    age: number;
}

function getUserInfo(): any {
    return {
      name: '听风是风',
      age: 28
    }
};

const user: UserInfo = getUserInfo();
</code></pre>
<p>单看类型断言和类型声明的补全的,你会发现后续使用<code>user</code>完全一致,那这两者有啥区别吗?我们再来看个例子:</p>
<pre><code class="language-typescript">interface Person {
    name: string;
}
interface User {
    name: string;
    age: number;
}

let echo: Person = {
    name: '听风是风'
}
// echo类型是Person,没有age属性
let xixi = echo as User;
// xixi此时能使用age属性了
xixi.age = 27
</code></pre>
<p>上述代码很明显<code>Person</code>兼容了<code>User</code>,因此变量<code>echo</code>我们能使用断言将其类型由<code>Person</code>转为<code>User</code>后再赋值给<code>xixi</code>,之后<code>xixi</code>就能赋予<code>age</code>属性了。</p>
<p>但假设上述代码我们通过类型声明来做,如下,你会发现报错了:</p>
<pre><code class="language-typescript">interface Person {
    name: string;
}
interface User {
    name: string;
    age: number;
}

let echo: Person = {
    name: '听风是风'
}
// error Person缺少age属性
let xixi: User = echo;
</code></pre>
<p>为什么报错?很明显<code>echo</code>的<code>Person</code>类没有<code>age</code>属性,而<code>User</code>需要<code>age</code>属性,回到断言限制的第一个例子,对比下你会发现,我们将一个属性更多的类赋值给一个属性更少的类可以(后者兼容前者),反过来则会报错。</p>
<p>结合类型断言,你会发现类型声明的限制比类型断言要严格的多,大致我们可以总结为:</p>
<ul>
<li>若<code>A</code>需要断言成<code>B</code>,只需要<code>A</code>兼容<code>B</code>或者<code>B</code>兼容<code>A</code>都可以</li>
<li>若将<code>A</code>赋值给<code>B</code>,那么一定是<code>B</code>兼容<code>A</code>才行(被赋值的类型属性比作为值的类型属性要少才行)。</li>
</ul>
<p>以上就是两者的区别。</p>
<h1 id="玖--内置对象">玖 ❀ 内置对象</h1>
<p>前文我们提到,在<code>js</code>中其实存在很多内置对象,比如<code>window、Date、RegExp</code>等等,你有没有想过,假设我现在声明了一个正则表达式,那我应该添加什么类型?</p>
<p>其实在<code>typescript</code>底层已经帮我们封装好了这些类型,我们可以直接使用,看部分例子:</p>
<pre><code class="language-typescript">let a: Boolean = new Boolean(1);
let b: Error = new Error('报错啦');
let c: Date = new Date();
let d: RegExp = /'听风是风'/;
</code></pre>
<p>可以看到通过<code>new</code>一个构造器得到的实例,它们的类型都是首字母大写,正则比较特殊,不管是对象声明还是<code>new</code>它本身就只支持<code>RegExp</code>。</p>
<p>再比如<code>js</code>中内置了一些<code>DOM</code>以及<code>BOM</code>对象,比如类数组<code>NodeList</code>,事件对象<code>Event</code>等,这些也能直接用于类型定义,比如:</p>
<pre><code class="language-typescript">let div:NodeList =document.querySelectorAll('.div');
</code></pre>
<p>更多内置对象类型查阅<code>MDN</code>,这里就不一一细说了。</p>
<h1 id="拾--总">拾 ❀ 总</h1>
<p>那么到这里,我们已经快速过完了<code>typescript</code>基础内容,准备来说这些知识已经足以支撑看懂项目中大部分<code>ts</code>代码了,毕竟<code>ts</code>也没有太多的额外知识,更多是对于<code>js</code>类型的限制,后续会再利用进阶篇补全剩下概念,那么到这里全文结束。</p><br><br>
来源:https://www.cnblogs.com/echolun/p/15956736.html
頁: [1]
查看完整版本: 快速上手typescript(基础篇)