历史性的时刻!OpenTiny 跨端、跨框架组件库正式升级 TypeScript,10 万行代码重获新生!
<p>大家好,我是 Kagol,OpenTiny 开源社区运营,TinyVue 跨端、跨框架组件库核心贡献者,专注于前端组件库建设和开源社区运营。</p><p>微软于3月16日发布了 TypeScript 5.0 版本。微软表示新版本体积更小、开发者更容易上手且运行速度更快。</p>
<p>根据 The Software House 发布的《2022 前端开发市场状态调查报告》数据显示,使用 TypeScript 的人数已经达到 84%,和 2021 年相比增加了 7 个百分点。</p>
<p>TypeScript 可谓逐年火热,使用者呈现逐年上升的趋势,再不学起来就说不过去。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/763b928360ba4880a96304469f2c06b8~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>通过本文你将收获:</p>
<ul>
<li>通过了解 TS 的四大好处,说服自己下定决心学习 TS</li>
<li>5 分钟学习 TS 最基础和常用的知识点,快速入门,包教包会</li>
<li>了解如何在 Vue 中使用 TypeScript,给 Vue2 开发者切换到 Vue3 + TypeScript 提供最基本的参考</li>
<li>如何将现有的 JS 项目改造成 TS 项目</li>
</ul>
<h2 id="1-学习-ts-的好处">1 学习 TS 的好处</h2>
<h3 id="11-好处一紧跟潮流让自己看起来很酷">1.1 好处一:紧跟潮流:让自己看起来很酷</h3>
<p>如果你没学过 TS<br>
你的前端朋友:都 2023 年了,你还不会 TS?给你一个眼色你自己感悟吧</p>
<p>如果你学过 TS<br>
你的前端朋友:哇,你们的项目已经用上 Vue3 + TS 啦,看起来真棒!教教我吧</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ac93d18687914b68aaba4f53e99934a3~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>如果说上面那个好处太虚了,那下面的3条好处可都是实实在在能让自己受益的。</p>
<h3 id="12-好处二智能提示提升开发者体验和效率">1.2 好处二:智能提示:提升开发者体验和效率</h3>
<p>当循环一个对象数组时,对象的属性列表可以直接显示出来,不用到对象的定义中去查询该对象有哪些属性。</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/baeec5f0a648462d96d246f20d18d625~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>通过调用后台接口获取的异步数据也可以通过TS类型进行智能提示,这样相当于集成了接口文档,后续后台修改字段,我们很容易就能发现。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1f3f4feb89e2488bad58a72e3084b110~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>Vue 组件的属性和事件都可以智能提示。</p>
<p>下图是我们 OpenTiny 跨端跨框架前端组件库中的 Alert 组件,当在组件标签中输入 <code>des</code> 时,会自动提示 <code>description</code> 属性;当输入 <code>@c</code> 时,会自动提示 <code>@close</code> 事件。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b0d61e0a1dbe42f087be606c071090cc~tplv-k3u1fbpfcp-watermark.image?"></p>
<h3 id="13-好处三错误标记代码哪里有问题一眼就知道">1.3 好处三:错误标记:代码哪里有问题一眼就知道</h3>
<p>在 JS 项目使用不存在的对象属性,在编码阶段不容易看出来,到运行时才会报错。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4fceb00a88d04ec1a0788a25c772dde4~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>在 TS 项目使用不存在的对象属性,在IDE中会有红色波浪线标记,鼠标移上去能看到具体的错误信息。</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c68990f2697a4799bd078fdda32d4923~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>在 JS 项目,调用方法时拼错单词不容易被发现,要在运行时才会将错误暴露出来。</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4ca1418afa1642e7841841122956c478~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>在 TS 项目会有红色波浪线提示,一眼就看出拼错单词。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d1dab83c118496190863c8aa971b9aa~tplv-k3u1fbpfcp-watermark.image?"></p>
<h3 id="14-好处四类型约束用我的代码就得听我的">1.4 好处四:类型约束:用我的代码就得听我的</h3>
<p>你写了一个工具函数 getType 给别人用,限定参数只能是指定的字符串,这时如果使用这个函数的人传入其他字符串,就会有红色波浪线提示。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/17ecc715b138410988e220dee4c7aaf2~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>Vue 组件也是一样的,可以限定组件 props 的类型,组件的使用者如果传入不正确的类型,将会有错误提示,比如:我们 OpenTiny 的 Alert 组件,closable 只能传入 Boolean 值,如果传入一个字符串就会有错误提示。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e9a5fbf42935486281035cccb2931096~tplv-k3u1fbpfcp-watermark.image?"></p>
<h2 id="2-极简-ts-基础5分钟学会">2 极简 TS 基础,5分钟学会</h2>
<p>以下内容虽然不多,但包含了实际项目开发中最实用的部分,对于 TS 入门者来说也是能很快学会的,学不会的找我,手把手教,包教包会,有手就会写。</p>
<h3 id="21-基本类型">2.1 基本类型</h3>
<p>用得较多的类型就下面5个,更多类型请参考:TS官网文档</p>
<ul>
<li>布尔 boolean</li>
<li>数值 number</li>
<li>字符串 string</li>
<li>空值 void:表示没有任何返回值的函数</li>
<li>任意 any:表示不被类型检查</li>
</ul>
<p>用法也很简单:</p>
<pre><code>let isDone: boolean = false;
let myFavoriteNumber: number = 6;
let myName: string = 'Kagol';
function alertName(name: string): void {
console.log(`My name is ${name}`);
}
</code></pre>
<p>默认情况下,name 会自动类型推导成 string 类型,此时如果给它赋值为一个 number 类型的值,会出现错误提示。</p>
<pre><code>let name = 'Kagol'
name = 6
</code></pre>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5576548c27764fa2b6b8011c76ca2e57~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>如果给 name 设置 any 类型,表示不做类型检查,这时错误提示消失。</p>
<pre><code>let name: any = 'Kagol'
name = 6
</code></pre>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/87d302560a7948318b6fefe14f82f5e7~tplv-k3u1fbpfcp-watermark.image?"></p>
<h3 id="22-函数">2.2 函数</h3>
<p>主要定义函数参数和返回值类型。</p>
<p>看一下例子:</p>
<pre><code>const sum = (x: number, y: number): number => {
return x + y
}
</code></pre>
<p>以上代码包含以下 TS 校验规则:</p>
<ul>
<li>调用 sum 函数时,必须传入两个参数,多一个或者少一个都不行</li>
<li>并且这两个参数的类型要为 number 类型</li>
<li>且函数的返回值为 number 类型</li>
</ul>
<p>少参数:</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/75df53ee931f472cb009009291d583d1~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>多参数:</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8cdc55e1cdf04e68aedf9e351dcd735b~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>参数类型错误:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a679a107d2f64670a0bd23e1f191cc28~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>返回值:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5105a22702ed461f9a56507b69cfb5d0~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>用问号 <code>?</code> 可以表示该参数是可选的。</p>
<pre><code>const sum = (x: number, y?: number): number => {
return x + (y || 0);
}
sum(1)
</code></pre>
<p>如果将 y 定义为可选参数,则调用 sum 函数时可以只传入一个参数。</p>
<p>需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了。</p>
<p>给 y 增加默认值 0 之后,y 会自动类型推导成 number 类型,不需要加 number 类型,并且由于有默认值,也不需要加可选参数。</p>
<pre><code>const sum = (x: number, y = 0): number => {
return x + y
}
sum(1)
sum(1, 2)
</code></pre>
<h3 id="23-数组">2.3 数组</h3>
<p>数组类型有两种表示方式:</p>
<ul>
<li><code>类型 + 方括号</code> 表示法</li>
<li><code>泛型</code> 表示法</li>
</ul>
<pre><code>// `类型 + 方括号` 表示法
let fibonacci: number[] =
// 泛型表示法
let fibonacci: Array<number> =
</code></pre>
<p>这两种都可以表示数组类型,看自己喜好进行选择即可。</p>
<p>如果是类数组,则不可以用数组的方式定义类型,因为它不是真的数组,需要用 interface 进行定义</p>
<pre><code>
interface IArguments {
: any;
length: number;
callee: Function;
}
function sum() {
let args: IArguments = arguments
}
</code></pre>
<p><code>IArguments</code> 类型已在 TypeScript 中内置,类似的还有很多:</p>
<pre><code>let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
// Do something
});
</code></pre>
<p>如果数组里的元素类型并不都是相同的怎么办呢?</p>
<p>这时 any 类型就发挥作用啦啦</p>
<pre><code>let list: any[] = ['OpenTiny', 112, { website: 'https://opentiny.design/' }];
</code></pre>
<h3 id="24-接口">2.4 接口</h3>
<p>接口简单理解就是一个对象的“轮廓”</p>
<pre><code>interface IResourceItem {
name: string;
value?: string | number;
total?: number;
checked?: boolean;
}
</code></pre>
<p>接口是可以继承接口的</p>
<pre><code>interface IClosableResourceItem extends IResourceItem {
closable?: boolean;
}
</code></pre>
<p>这样 IClosableResourceItem 就包含了 IResourceItem 属性和自己的 closable 可选属性。</p>
<p>接口也是可以被类实现的</p>
<pre><code>interface Alarm {
alert(): void;
}
class Door {
}
class SecurityDoor extends Door implements Alarm {
alert() {
console.log('SecurityDoor alert')
}
}
</code></pre>
<p>如果类实现了一个接口,却不写具体的实现代码,则会有错误提示</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b891a77be1944ceb90f8884bc637d1a8~tplv-k3u1fbpfcp-watermark.image?"></p>
<h3 id="25-联合类型--类型别名">2.5 联合类型 & 类型别名</h3>
<p>联合类型是指取值可以为多种类型中的一种,而类型别名常用于联合类型。</p>
<p>看以下例子:</p>
<pre><code>// 联合类型
let myFavoriteNumber: string | number
myFavoriteNumber = 'six'
myFavoriteNumber = 6
// 类型别名
type FavoriteNumber = string | number
let myFavoriteNumber: FavoriteNumber
</code></pre>
<p>当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:</p>
<pre><code>function getLength(something: string | number): number {
return something.length
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
</code></pre>
<p>上例中,length 不是 string 和 number 的共有属性,所以会报错。</p>
<p>访问 string 和 number 的共有属性是没问题的:</p>
<pre><code>function getString(something: string | number): string {
return something.toString()
}
</code></pre>
<h3 id="26-类型断言">2.6 类型断言</h3>
<p>类型断言(Type Assertion)可以用来手动指定一个值的类型。</p>
<p>语法:值 as 类型,比如:<code>(animal as Fish).swim()</code></p>
<p>类型断言主要有以下用途:</p>
<ul>
<li>将一个联合类型断言为其中一个类型</li>
<li>将一个父类断言为更加具体的子类</li>
<li>将任何一个类型断言为 any</li>
<li>将 any 断言为一个具体的类型</li>
</ul>
<p>我们一个个来看。</p>
<p>用途1:将一个联合类型断言为其中一个类型</p>
<pre><code>interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
const animal: Cat | Fish = new Animal()
animal.swim()
</code></pre>
<p>animal 是一个联合类型,可能是猫 Cat,也可能是鱼 Fish,如果直接调用 swim 方法是要出现错误提示的,因为猫不会游泳。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6b9b5a03b530446aa9376be78b1e7f5d~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>这时类型断言就派上用场啦啦,因为调用的是 swim 方法,那肯定是鱼,所以直接断言为 Fish 就不会出现错误提示。</p>
<pre><code>const animal: Cat | Fish = new Animal()
(animal as Fish).swim()
</code></pre>
<p>用途2:将一个父类断言为更加具体的子类</p>
<pre><code>
class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
}
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
</code></pre>
<p>ApiError 和 HttpError 都继承自 Error 父类,error 变量的类型是 Error,去取 code 变量肯定是不行,因为取的是 code 变量,我们可以直接断言为 ApiError 类型。</p>
<p>用途3:将任何一个类型断言为 any</p>
<p>这个非常有用,看一下例子:</p>
<pre><code>function getCacheData(key: string): any {
return (window as any).cache;
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
</code></pre>
<p>getCacheData 是一个历史遗留函数,不是你写的,由于他返回 any 类型,就等于放弃了 TS 的类型检验,假如 tom 是一只猫,里面有 name 属性和 <code>run()</code> 方法,但由于返回 any 类型,<code>tom.</code> 是没有任何提示的。</p>
<p>如果将其断言为 Cat 类型,就可以 <code>点</code> 出 name 属性和 <code>run()</code> 方法。</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4912680d794b48b1b6f6e946568b7323~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>用途4:将 any 断言为一个具体的类型</p>
<p>这个比较常见的场景是给 window 挂在一个自己的变量和方法。</p>
<pre><code>window.foo = 1;
// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
(window as any).foo = 1;
</code></pre>
<p>由于 window 下没有 foo 变量,直接赋值会有错误提示,将 window 断言为 any 就没问题啦啦。</p>
<h3 id="27-元组">2.7 元组</h3>
<p>数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。</p>
<pre><code>let tom: = ['Tom', 25];
</code></pre>
<p>给元组类型赋值时,数组每一项的类型需要和元组定义的类型对应上。</p>
<p>当赋值或访问一个已知索引的元素时,会得到正确的类型:</p>
<pre><code>let tom: ;
tom = 'Tom';
tom = 25;
tom.slice(1);
tom.toFixed(2);
</code></pre>
<p>也可以只赋值其中一项:</p>
<pre><code>let tom: ;
tom = 'Tom';
</code></pre>
<p>但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。</p>
<pre><code>let tom: ;
tom = ['Tom'];
// Property '1' is missing in type '' but required in type ''.
</code></pre>
<p>当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:</p>
<pre><code>let tom: ;
tom = ['Tom', 25];
tom.push('male');
tom.push(true);
// Argument of type 'true' is not assignable to parameter of type 'string | number'.
</code></pre>
<p>push 字符串和数字都可以,布尔就不行。</p>
<h3 id="28-枚举">2.8 枚举</h3>
<p>枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。</p>
<pre><code>enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}
</code></pre>
<p>枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:</p>
<pre><code>console.log(Days.Sun === 0) // true
console.log(Days === 'Sun') // true
console.log('Days', Days)
</code></pre>
<p>手动赋值:未手动赋值的枚举项会接着上一个枚举项递增。</p>
<pre><code>enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat}
</code></pre>
<h3 id="29-类">2.9 类</h3>
<p>给类加上 TypeScript 的类型很简单,与接口类似:</p>
<pre><code>class Animal {
name: string
constructor(name: string) {
this.name = name
}
sayHi(welcome: string): string {
return `${welcome} My name is ${this.name}`
}
}
</code></pre>
<p>类的语法涉及到较多概念,请参考:</p>
<ul>
<li>https://es6.ruanyifeng.com/#docs/class</li>
<li>https://ts.xcatliu.com/advanced/class.html</li>
</ul>
<h3 id="210-泛型">2.10 泛型</h3>
<p>泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。</p>
<p>可以简单理解为定义函数时的形参。</p>
<p>设想以下场景,我们有一个 print 函数,输入什么,原样打印,函数的入参和返回值类型是一致的。</p>
<p>一开始只需要打印字符串:</p>
<pre><code>function print(arg: string): string {
return arg
}
</code></pre>
<p>后面需求变了,除了能打印字符串,还要能打印数字:</p>
<pre><code>function print(arg: string | number): string | number {
return arg
}
</code></pre>
<p>假如需求又变了,要打印布尔值、对象、数组,甚至自定义的类型,怎么办,写一串联合类型?显然是不可取的,用 any?那就失去了 TS 类型校验能力,沦为 JS。</p>
<p>function print(arg: any): any {<br>
return arg<br>
}</p>
<p>解决这个问题的完美方法就是泛型!</p>
<p>print 后面加上一对尖括号,里面写一个 T,这个 T 就类似是一个类型的形参。</p>
<p>这个类型形参可以在函数入参里用,也可以在函数返回值使用,甚至也可以在函数体里面的变量、函数里面用。</p>
<pre><code>function print<T>(arg: T): T {
return arg
}
</code></pre>
<p>那么实参哪里来?用的时候传进来!</p>
<pre><code>const res = print<number>(123)
</code></pre>
<p>我们还可以使用泛型来约束后端接口参数类型。</p>
<pre><code>import axios from 'axios'
interface API {
'/book/detail': {
id: number,
},
'/book/comment': {
id: number
comment: string
}
...
}
function request<T extends keyof API>(url: T, obj: API) {
return axios.post(url, obj)
}
request('/book/comment', {
id: 1,
comment: '非常棒!'
})
</code></pre>
<p>以上代码对接口进行了约束:</p>
<ul>
<li>url 只能是 API 中定义过的,其他 url 都会提示错误</li>
</ul>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2485db8d0ad047a79f0defa846373286~tplv-k3u1fbpfcp-watermark.image?"></p>
<ul>
<li>接口参数 obj 必须和 url 能对应上,不能少属性,属性类型也不能错</li>
</ul>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/91a1261517d44802b22ef29d47be6583~tplv-k3u1fbpfcp-watermark.image?"></p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bcf9fc556b46435bbaa0dfe29cb9764a~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>而且调用 request 方法时,也会提示 url 可以选择哪些</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/57f407a7d69d456f9d902cc35467597f~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>如果后台改了接口参数名,我们一眼就看出来了,都不用去找接口文档,是不是很厉害!</p>
<p>泛型的例子参考了前端阿林的文章:</p>
<ul>
<li>轻松拿下 TS 泛型</li>
</ul>
<h2 id="3-ts-在-vue-中的实践">3 TS 在 Vue 中的实践</h2>
<h3 id="31-定义组件-props-的类型">3.1 定义组件 props 的类型</h3>
<p>不使用 setup 语法糖</p>
<pre><code>export default defineComponent({
props: {
items: {
type: Object as PropType<IResourceItem[]>,
default() {
return []
}
},
span: {
type: Number,
default: 4
},
gap: {
type: as PropType<string | number>,
default: '12px'
},
block: {
type: Object as PropType<Component>,
default: TvpBlock
},
beforeClose: Function as PropType<() => boolean>
}
})
</code></pre>
<p>使用 setup 语法糖 – runtime 声明</p>
<pre><code>import { PropType, Component } from 'vue'
const props = defineProps({
items: {
type: Object as PropType<IResourceItem[]>,
default() {
return []
}
},
span: {
type: Number,
default: 4
},
gap: {
type: as PropType<string | number>,
default: '12px'
},
block: {
type: Object as PropType<Component>,
default: TvpBlock
},
beforeClose: Function as PropType<() => boolean>
})
</code></pre>
<p>使用 setup 语法糖 – type-based 声明</p>
<pre><code>import { Component, withDefaults } from 'vue'
interface Props {
items: IResourceItem[]
span: number
gap: string | number
block: Component
beforeClose: () => void
}
const props = withDefaults(defineProps<Props>(), {
items: () => [],
span: 4,
gap: '12px',
block: TvpBlock
})
</code></pre>
<p>IResourceItem:</p>
<pre><code>interface IResourceItem {
name: string;
value?: string | number;
total?: number;
checked?: boolean;
closable?: boolean;
}
</code></pre>
<h3 id="32-定义-emits-类型">3.2 定义 emits 类型</h3>
<p>不使用 setup 语法糖</p>
<pre><code>export default defineComponent({
emits: ['change', 'update'],
setup(props, { emit }) {
emit('change')
}
})
</code></pre>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/26fccfbbb5554f168008ed18e25087d3~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>使用 setup 语法糖</p>
<pre><code><script setup lang="ts">
// runtime
const emit = defineEmits(['change', 'update'])
// type-based
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
</code></pre>
<h3 id="33-定义-ref-类型">3.3 定义 ref 类型</h3>
<p>默认会自动进行类型推导</p>
<pre><code>import { ref } from 'vue'
// inferred type: Ref<number>
const year = ref(2020)
// => TS Error: Type 'string' is not assignable to type 'number'.
year.value = '2020'
</code></pre>
<p>两种声明 ref 类型的方法</p>
<pre><code>import { ref } from 'vue'
import type { Ref } from 'vue'
// 方式一
const year: Ref<string | number> = ref('2020')
year.value = 2020 // ok!
// 方式二
// resulting type: Ref<string | number>
const year = ref<string | number>('2020')
year.value = 2020 // ok!
</code></pre>
<h3 id="34-定义-reactive-类型">3.4 定义 reactive 类型</h3>
<p>默认会自动进行类型推导</p>
<pre><code>import { reactive } from 'vue'
// inferred type: { title: string }
const book = reactive({ title: 'Vue 3 Guide' })
</code></pre>
<p>使用接口定义明确的类型</p>
<pre><code>import { reactive } from 'vue'
interface Book {
title: string
year?: number
}
const book: Book = reactive({ title: 'Vue 3 Guide' })
</code></pre>
<h3 id="35-定义-computed-类型">3.5 定义 computed 类型</h3>
<p>默认会自动进行类型推导</p>
<pre><code>import { ref, computed } from 'vue'
const count = ref(0)
// inferred type: ComputedRef<number>
const double = computed(() => count.value * 2)
// => TS Error: Property 'split' does not exist on type 'number'
const result = double.value.split('')
</code></pre>
<p>两种声明 computed 类型的方法</p>
<pre><code>import { ComputedRef, computed } from 'vue'
const double: ComputedRef<number> = computed(() => {
// type error if this doesn't return a number
})
const double = computed<number>(() => {
// type error if this doesn't return a number
})
</code></pre>
<h3 id="36-定义-provideinject-类型">3.6 定义 provide/inject 类型</h3>
<p>provide</p>
<pre><code>import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'
// 声明 provide 的值为 string 类型
const key = Symbol() as InjectionKey<string>
provide(key, 'foo') // providing non-string value will result in error
</code></pre>
<p>inject</p>
<pre><code>// 自动推导为 string 类型
const foo = inject(key) // type of foo: string | undefined
// 明确指定为 string 类型
const foo = inject<string>('foo') // type: string | undefined
// 增加默认值
const foo = inject<string>('foo', 'bar') // type: string
// 类型断言为 string
const foo = inject('foo') as string
</code></pre>
<h3 id="37-定义模板引用的类型">3.7 定义模板引用的类型</h3>
<pre><code><script setup lang="ts">
import { ref, onMounted } from 'vue'
const el = ref<HTMLInputElement | null>(null)
onMounted(() => {
el.value?.focus()
})
</script>
<template>
<input ref="el" />
</template>
</code></pre>
<h3 id="38-定义组件模板引用的类型">3.8 定义组件模板引用的类型</h3>
<p>定义一个 MyModal 组件</p>
<pre><code><!-- MyModal.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const isContentShown = ref(false)
const open = () => (isContentShown.value = true)
defineExpose({
open
})
</script>
</code></pre>
<p>在 App.vue 中引用 MyModal 组件</p>
<pre><code><!-- App.vue -->
<script setup lang="ts">
import MyModal from './MyModal.vue'
const modal = ref<InstanceType<typeof MyModal> | null>(null)
const openModal = () => {
modal.value?.open()
}
</script>
</code></pre>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7e93a2ca9b954aa18be7efcdecc3e364~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>参考 Vue 官网文档:</p>
<ul>
<li>TypeScript with Composition API</li>
</ul>
<h2 id="4-js-项目转-ts">4 JS 项目转 TS</h2>
<p>还是使用 JS 的同学有福啦!为了让大家快速用上 TS,享受 TS 的丝滑体验,我整理了一份<code>《JS 项目改造成 TS 项目指南》</code>。有了这份步骤指南,JS 项目转 TS 不再是难事!</p>
<p>我们新开源的 TinyVue 组件库,就使用这份<code>《JS 项目改造成 TS 项目指南》</code>,成功地由 JS 项目改造成了 TS 项目,悄悄地告诉大家:</p>
<ul>
<li>TinyVue 是一套跨端、跨框架的企业级 UI 组件库,支持 Vue 2 和 Vue 3,支持 PC 端和移动端。</li>
<li>在内部经过9年持续打磨,服务于华为内外部上千个项目。</li>
<li>目前代码量超过<code>10万行</code>。</li>
</ul>
<p>这么庞大的代码量都能从 JS 转 TS,其他小规模的项目更是不在话下。</p>
<p>为了验证自己的猜想,我又在 GitHub 找到了一个6年前的 Vue2 + JS 项目,目前早已不再维护,打算尝试将其改造成 TS 项目,结果按照这份指南,1个小时不用就搞定啦啦</p>
<p>https://github.com/liangxiaojuan/vue-todos</p>
<p>这个项目的效果图长这样:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58afe2ae92e5403aa03fcf2fb431e821~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>我已经提了 issue,看下作者是否同意改造成 TS,同意的话,我立马就是一个 PR 过去!</p>
<p>话不多说,大家有需要的,可直接拿走!</p>
<h3 id="js-项目改造成-ts-项目指南">《JS 项目改造成 TS 项目指南》</h3>
<p>JS 项目改造成 TS 步骤:</p>
<ol>
<li>安装 TS:<code>npm i typescript ts-loader -D</code></li>
<li>增加 TS 配置文件:<code>tsconfig.json</code></li>
<li>修改文件后缀名:<code>x.js -> x.ts</code></li>
<li><code>x.vue</code> 文件增加 lang:<code><script lang="ts"></code></li>
<li><code>vite.config.js</code> 配置后缀名</li>
<li>升级依赖,修改本地启动和构建脚本</li>
<li>添加 <code>loader</code> / <code>plugin</code> 等</li>
<li>逐步补充类型声明</li>
</ol>
<p>tsconfig.ts</p>
<pre><code>{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true
},
"include": [
"src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"
]
}
</code></pre>
<p>配置文件后缀名,增加 <code>.ts</code> 和 <code>.tsx</code></p>
<pre><code>extensions: ['.js', '.vue', '.json', '.ts', 'tsx'],
</code></pre>
<p>入口文件要由 <code>main.js</code> 改成 <code>main.ts</code></p>
<pre><code>entry: {
app: './src/main.ts'
},
</code></pre>
<p>需要配置下 loader</p>
<pre><code>{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
appendTsSuffixTo: [/\.vue$/]
},
include:
}
</code></pre>
<p>以及 plugin</p>
<pre><code>const { VueLoaderPlugin } = require('vue-loader')
plugins: [
new VueLoaderPlugin()
],
</code></pre>
<p>完成之后,先测试下项目是否能正常启动和构建:<code>npm run dev</code> / <code>npm run build</code></p>
<p>都没问题之后,本次 TS 项目改造就完成大部分啦啦!</p>
<p>后续就是逐步补充代码涉及到的变量和函数的类型声明即可。</p>
<p>改造过程中遇到问题欢迎留言讨论,希望你也能尽快享受 TS 的丝滑开发者体验!</p>
<h2 id="tinyvue-招募贡献者啦">TinyVue 招募贡献者啦</h2>
<p>如果你对我们的跨端跨框架组件库 TinyVue 感兴趣,欢迎参与到我们的开源社区中来,一起将它建设得更好!</p>
<p>参与 TinyVue 组件库建设,你将收获:</p>
<p>直接的价值:</p>
<ul>
<li>通过打造一个跨端、跨框架的组件库项目,学习最新的 <code>Monorepo</code> + <code>Vite</code> + <code>Vue3</code> + <code>TypeScript</code> 技术</li>
<li>学习从 0 到 1 搭建一个自己的组件库的整套流程和方法论,包括组件库工程化、组件的设计和开发等</li>
<li>为自己的简历和职业生涯添彩,参与过优秀的开源项目,这本身就是受面试官青睐的亮点</li>
<li>结识一群优秀的、热爱学习、热爱开源的小伙伴,大家一起打造一个伟大的产品</li>
</ul>
<p>长远的价值:</p>
<ul>
<li>打造个人品牌,提升个人影响力</li>
<li>培养良好的编码习惯</li>
<li>获得华为云 OpenTiny 开源社区的荣誉&认可和定制小礼物</li>
<li>成为 PMC & Committer 之后还能参与 OpenTiny 整个开源生态的决策和长远规划,培养自己的管理和规划能力<br>
未来有更多机会和可能</li>
</ul>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ed49b40da291491586b54e82c159826d~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>往期活动礼品及贡献者的反馈:</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3ba8b65600b47feaf49e2035cac9ab3~tplv-k3u1fbpfcp-watermark.image?"></p>
<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/18d1b463adff4c9c95efdc1997bccd5f~tplv-k3u1fbpfcp-watermark.image?">
<h2 id="联系我们">联系我们</h2>
<p>如果你对我们 OpenTiny 的开源项目感兴趣,欢迎添加小助手微信:opentiny-official,拉你进群,一起交流前端技术,一起玩开源。</p>
<p>官网:https://opentiny.design/</p>
<p>GitHub仓库:https://github.com/opentiny/</p>
<p>TinyVue:https://github.com/opentiny/tiny-vue(欢迎 Star 🌟)</p><br><br>
来源:https://www.cnblogs.com/kagol/p/17271186.html
頁:
[1]