歷史性的時(shí)刻!OpenTiny 跨端、跨框架組件庫(kù)正式升級(jí) TypeScript,10 萬(wàn)行代碼重獲新
大家好,我是 Kagol,OpenTiny?開(kāi)源社區(qū)運(yùn)營(yíng),TinyVue?跨端、跨框架組件庫(kù)核心貢獻(xiàn)者,專注于前端組件庫(kù)建設(shè)和開(kāi)源社區(qū)運(yùn)營(yíng)。
微軟于3月16日發(fā)布了 TypeScript 5.0?版本。微軟表示新版本體積更小、開(kāi)發(fā)者更容易上手且運(yùn)行速度更快。
根據(jù) The Software House 發(fā)布的《2022 前端開(kāi)發(fā)市場(chǎng)狀態(tài)調(diào)查報(bào)告》數(shù)據(jù)顯示,使用 TypeScript 的人數(shù)已經(jīng)達(dá)到 84%,和 2021 年相比增加了 7 個(gè)百分點(diǎn)。
TypeScript 可謂逐年火熱,使用者呈現(xiàn)逐年上升的趨勢(shì),再不學(xué)起來(lái)就說(shuō)不過(guò)去。
我們?OpenTiny?近期做了一次大的升級(jí),將原來(lái)運(yùn)行了?9年
?的?JavaScript
?代碼升級(jí)到了?TypeScript
,并通過(guò) Monorepo 進(jìn)行子包的管理,還在用 JavaScript 的朋友抓緊升級(jí)哦,我特意準(zhǔn)備了一份《JS項(xiàng)目改造TS指南》文檔供大家參考,順便介紹了一些 TS 基礎(chǔ)知識(shí)和 TS 在 Vue 中的一些實(shí)踐。

通過(guò)本文你將收獲:
通過(guò)了解 TS 的四大好處,說(shuō)服自己下定決心學(xué)習(xí) TS
5 分鐘學(xué)習(xí) TS 最基礎(chǔ)和常用的知識(shí)點(diǎn),快速入門(mén),包教包會(huì)
了解如何在 Vue 中使用 TypeScript,給 Vue2 開(kāi)發(fā)者切換到 Vue3 + TypeScript 提供最基本的參考
如何將現(xiàn)有的 JS 項(xiàng)目改造成 TS 項(xiàng)目
1 學(xué)習(xí) TS 的好處
1.1 好處一:緊跟潮流:讓自己看起來(lái)很酷
如果你沒(méi)學(xué)過(guò) TS
你的前端朋友:都 2023 年了,你還不會(huì) TS?給你一個(gè)眼色你自己感悟吧
如果你學(xué)過(guò) TS
你的前端朋友:哇,你們的項(xiàng)目已經(jīng)用上 Vue3 + TS 啦,看起來(lái)真棒!教教我吧

如果說(shuō)上面那個(gè)好處太虛了,那下面的3條好處可都是實(shí)實(shí)在在能讓自己受益的。
1.2 好處二:智能提示:提升開(kāi)發(fā)者體驗(yàn)和效率
當(dāng)循環(huán)一個(gè)對(duì)象數(shù)組時(shí),對(duì)象的屬性列表可以直接顯示出來(lái),不用到對(duì)象的定義中去查詢?cè)搶?duì)象有哪些屬性。

通過(guò)調(diào)用后臺(tái)接口獲取的異步數(shù)據(jù)也可以通過(guò)TS類型進(jìn)行智能提示,這樣相當(dāng)于集成了接口文檔,后續(xù)后臺(tái)修改字段,我們很容易就能發(fā)現(xiàn)。

Vue 組件的屬性和事件都可以智能提示。
下圖是我們?OpenTiny?跨端跨框架前端組件庫(kù)中的 Alert 組件,當(dāng)在組件標(biāo)簽中輸入?des
?時(shí),會(huì)自動(dòng)提示?description
?屬性;當(dāng)輸入?@c
?時(shí),會(huì)自動(dòng)提示?@close
?事件。

1.3 好處三:錯(cuò)誤標(biāo)記:代碼哪里有問(wèn)題一眼就知道
在 JS 項(xiàng)目使用不存在的對(duì)象屬性,在編碼階段不容易看出來(lái),到運(yùn)行時(shí)才會(huì)報(bào)錯(cuò)。

在 TS 項(xiàng)目使用不存在的對(duì)象屬性,在IDE中會(huì)有紅色波浪線標(biāo)記,鼠標(biāo)移上去能看到具體的錯(cuò)誤信息。

在 JS 項(xiàng)目,調(diào)用方法時(shí)拼錯(cuò)單詞不容易被發(fā)現(xiàn),要在運(yùn)行時(shí)才會(huì)將錯(cuò)誤暴露出來(lái)。

在 TS 項(xiàng)目會(huì)有紅色波浪線提示,一眼就看出拼錯(cuò)單詞。

1.4 好處四:類型約束:用我的代碼就得聽(tīng)我的
你寫(xiě)了一個(gè)工具函數(shù) getType 給別人用,限定參數(shù)只能是指定的字符串,這時(shí)如果使用這個(gè)函數(shù)的人傳入其他字符串,就會(huì)有紅色波浪線提示。

Vue 組件也是一樣的,可以限定組件 props 的類型,組件的使用者如果傳入不正確的類型,將會(huì)有錯(cuò)誤提示,比如:我們?OpenTiny?的 Alert 組件,closable 只能傳入 Boolean 值,如果傳入一個(gè)字符串就會(huì)有錯(cuò)誤提示。

2 極簡(jiǎn) TS 基礎(chǔ),5分鐘學(xué)會(huì)
以下內(nèi)容雖然不多,但包含了實(shí)際項(xiàng)目開(kāi)發(fā)中最實(shí)用的部分,對(duì)于 TS 入門(mén)者來(lái)說(shuō)也是能很快學(xué)會(huì)的,學(xué)不會(huì)的找我,手把手教,包教包會(huì),有手就會(huì)寫(xiě)。
2.1 基本類型
用得較多的類型就下面5個(gè),更多類型請(qǐng)參考:TS官網(wǎng)文檔
布爾 boolean
數(shù)值 number
字符串 string
空值 void:表示沒(méi)有任何返回值的函數(shù)
任意 any:表示不被類型檢查
用法也很簡(jiǎn)單:
let isDone: boolean = false; let myFavoriteNumber: number = 6; let myName: string = 'Kagol'; function alertName(name: string): void { ? ? console.log(`My name is ${name}`); ? }
默認(rèn)情況下,name 會(huì)自動(dòng)類型推導(dǎo)成 string 類型,此時(shí)如果給它賦值為一個(gè) number 類型的值,會(huì)出現(xiàn)錯(cuò)誤提示。
let name = 'Kagol' ? name = 6

如果給 name 設(shè)置 any 類型,表示不做類型檢查,這時(shí)錯(cuò)誤提示消失。
let name: any = 'Kagol' ? name = 6

2.2 函數(shù)
主要定義函數(shù)參數(shù)和返回值類型。
看一下例子:
const sum = (x: number, y: number): number => { ? ? return x + y ? }
以上代碼包含以下 TS 校驗(yàn)規(guī)則:
調(diào)用 sum 函數(shù)時(shí),必須傳入兩個(gè)參數(shù),多一個(gè)或者少一個(gè)都不行
并且這兩個(gè)參數(shù)的類型要為 number 類型
且函數(shù)的返回值為 number 類型
少參數(shù):
多參數(shù):
參數(shù)類型錯(cuò)誤:
返回值:
用問(wèn)號(hào)??
?可以表示該參數(shù)是可選的。
const sum = (x: number, y?: number): number => { ? ? return x + (y || 0); ? } ? sum(1) ?
如果將 y 定義為可選參數(shù),則調(diào)用 sum 函數(shù)時(shí)可以只傳入一個(gè)參數(shù)。
需要注意的是,可選參數(shù)必須接在必需參數(shù)后面。換句話說(shuō),可選參數(shù)后面不允許再出現(xiàn)必需參數(shù)了。
給 y 增加默認(rèn)值 0 之后,y 會(huì)自動(dòng)類型推導(dǎo)成 number 類型,不需要加 number 類型,并且由于有默認(rèn)值,也不需要加可選參數(shù)。
const sum = (x: number, y = 0): number => { ? ? return x + y ? } sum(1) ? sum(1, 2) ?
2.3 數(shù)組
數(shù)組類型有兩種表示方式:
類型 + 方括號(hào)
?表示法泛型
?表示法
// `類型 + 方括號(hào)` 表示法 let fibonacci: number[] = [1, 1, 2, 3, 5] // 泛型表示法 let fibonacci: Array<number> = [1, 1, 2, 3, 5]
這兩種都可以表示數(shù)組類型,看自己喜好進(jìn)行選擇即可。
如果是類數(shù)組,則不可以用數(shù)組的方式定義類型,因?yàn)樗皇钦娴臄?shù)組,需要用 interface 進(jìn)行定義
interface IArguments { ? [index: number]: any; ? length: number; ? callee: Function; } function sum() { ? let args: IArguments = arguments }
IArguments
?類型已在 TypeScript 中內(nèi)置,類似的還有很多:
let body: HTMLElement = document.body; let allDiv: NodeList = document.querySelectorAll('div'); document.addEventListener('click', function(e: MouseEvent) { ? // Do something });
如果數(shù)組里的元素類型并不都是相同的怎么辦呢?
這時(shí) any 類型就發(fā)揮作用啦啦
let list: any[] = ['OpenTiny', 112, { website: 'https://opentiny.design/' }];
2.4 接口
接口簡(jiǎn)單理解就是一個(gè)對(duì)象的“輪廓”
interface IResourceItem { ? name: string; ? value?: string | number; ? total?: number; ? checked?: boolean; }
接口是可以繼承接口的
interface IClosableResourceItem extends IResourceItem { ? closable?: boolean; }
這樣 IClosableResourceItem 就包含了 IResourceItem 屬性和自己的 closable 可選屬性。
接口也是可以被類實(shí)現(xiàn)的
interface Alarm { ? alert(): void; } class Door { } class SecurityDoor extends Door implements Alarm { ? alert() { ? ? console.log('SecurityDoor alert') ? } }
如果類實(shí)現(xiàn)了一個(gè)接口,卻不寫(xiě)具體的實(shí)現(xiàn)代碼,則會(huì)有錯(cuò)誤提示
2.5 聯(lián)合類型 & 類型別名
聯(lián)合類型是指取值可以為多種類型中的一種,而類型別名常用于聯(lián)合類型。
看以下例子:
// 聯(lián)合類型 let myFavoriteNumber: string | number myFavoriteNumber = 'six' myFavoriteNumber = 6 // 類型別名 type FavoriteNumber = string | number let myFavoriteNumber: FavoriteNumber
當(dāng) TypeScript 不確定一個(gè)聯(lián)合類型的變量到底是哪個(gè)類型的時(shí)候,我們只能訪問(wèn)此聯(lián)合類型的所有類型里共有的屬性或方法:
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'.
上例中,length 不是 string 和 number 的共有屬性,所以會(huì)報(bào)錯(cuò)。
訪問(wèn) string 和 number 的共有屬性是沒(méi)問(wèn)題的:
function getString(something: string | number): string { ? return something.toString() }
2.6 類型斷言
類型斷言(Type Assertion)可以用來(lái)手動(dòng)指定一個(gè)值的類型。
語(yǔ)法:值 as 類型,比如:(animal as Fish).swim()
類型斷言主要有以下用途:
將一個(gè)聯(lián)合類型斷言為其中一個(gè)類型
將一個(gè)父類斷言為更加具體的子類
將任何一個(gè)類型斷言為 any
將 any 斷言為一個(gè)具體的類型
我們一個(gè)個(gè)來(lái)看。
用途1:將一個(gè)聯(lián)合類型斷言為其中一個(gè)類型
interface Cat { ? name: string; ? run(): void; } interface Fish { ? name: string; ? swim(): void; } const animal: Cat | Fish = new Animal() animal.swim()
animal 是一個(gè)聯(lián)合類型,可能是貓 Cat,也可能是魚(yú) Fish,如果直接調(diào)用 swim 方法是要出現(xiàn)錯(cuò)誤提示的,因?yàn)樨埐粫?huì)游泳。
這時(shí)類型斷言就派上用場(chǎng)啦啦,因?yàn)檎{(diào)用的是 swim 方法,那肯定是魚(yú),所以直接斷言為 Fish 就不會(huì)出現(xiàn)錯(cuò)誤提示。
const animal: Cat | Fish = new Animal() (animal as Fish).swim()
用途2:將一個(gè)父類斷言為更加具體的子類
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; }
ApiError 和 HttpError 都繼承自 Error 父類,error 變量的類型是 Error,去取 code 變量肯定是不行,因?yàn)槿〉氖?code 變量,我們可以直接斷言為 ApiError 類型。
用途3:將任何一個(gè)類型斷言為 any
這個(gè)非常有用,看一下例子:
function getCacheData(key: string): any { ? return (window as any).cache[key]; } interface Cat { ? name: string; ? run(): void; } const tom = getCacheData('tom') as Cat;
getCacheData 是一個(gè)歷史遺留函數(shù),不是你寫(xiě)的,由于他返回 any 類型,就等于放棄了 TS 的類型檢驗(yàn),假如 tom 是一只貓,里面有 name 屬性和?run()
?方法,但由于返回 any 類型,tom.
?是沒(méi)有任何提示的。
如果將其斷言為 Cat 類型,就可以?點(diǎn)
?出 name 屬性和?run()
?方法。
用途4:將 any 斷言為一個(gè)具體的類型
這個(gè)比較常見(jiàn)的場(chǎng)景是給 window 掛在一個(gè)自己的變量和方法。
window.foo = 1; // index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'. (window as any).foo = 1;
由于 window 下沒(méi)有 foo 變量,直接賦值會(huì)有錯(cuò)誤提示,將 window 斷言為 any 就沒(méi)問(wèn)題啦啦。
2.7 元組
數(shù)組合并了相同類型的對(duì)象,而元組(Tuple)合并了不同類型的對(duì)象。
let tom: [string, number] = ['Tom', 25];
給元組類型賦值時(shí),數(shù)組每一項(xiàng)的類型需要和元組定義的類型對(duì)應(yīng)上。
當(dāng)賦值或訪問(wèn)一個(gè)已知索引的元素時(shí),會(huì)得到正確的類型:
let tom: [string, number]; tom[0] = 'Tom'; tom[1] = 25; tom[0].slice(1); tom[1].toFixed(2);
也可以只賦值其中一項(xiàng):
let tom: [string, number]; tom[0] = 'Tom';
但是當(dāng)直接對(duì)元組類型的變量進(jìn)行初始化或者賦值的時(shí)候,需要提供所有元組類型中指定的項(xiàng)。
let tom: [string, number]; tom = ['Tom']; // Property '1' is missing in type '[string]' but required in type '[string, number]'.
當(dāng)添加越界的元素時(shí),它的類型會(huì)被限制為元組中每個(gè)類型的聯(lián)合類型:
let tom: [string, number]; tom = ['Tom', 25]; tom.push('male'); tom.push(true); // Argument of type 'true' is not assignable to parameter of type 'string | number'.
push 字符串和數(shù)字都可以,布爾就不行。
2.8 枚舉
枚舉(Enum)類型用于取值被限定在一定范圍內(nèi)的場(chǎng)景,比如一周只能有七天,顏色限定為紅綠藍(lán)等。
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}
枚舉成員會(huì)被賦值為從 0 開(kāi)始遞增的數(shù)字,同時(shí)也會(huì)對(duì)枚舉值到枚舉名進(jìn)行反向映射:
console.log(Days.Sun === 0) // true console.log(Days[0] === 'Sun') // true console.log('Days', Days)
手動(dòng)賦值:未手動(dòng)賦值的枚舉項(xiàng)會(huì)接著上一個(gè)枚舉項(xiàng)遞增。
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat}
2.9 類
給類加上 TypeScript 的類型很簡(jiǎn)單,與接口類似:
class Animal { ? name: string ? constructor(name: string) { ? ? this.name = name ? } ? sayHi(welcome: string): string { ? ? return `${welcome} My name is ${this.name}` ? } }
類的語(yǔ)法涉及到較多概念,請(qǐng)參考:
https://es6.ruanyifeng.com/#docs/class
https://ts.xcatliu.com/advanced/class.html
2.10 泛型
泛型(Generics)是指在定義函數(shù)、接口或類的時(shí)候,不預(yù)先指定具體的類型,而在使用的時(shí)候再指定類型的一種特性。
可以簡(jiǎn)單理解為定義函數(shù)時(shí)的形參。
設(shè)想以下場(chǎng)景,我們有一個(gè) print 函數(shù),輸入什么,原樣打印,函數(shù)的入?yún)⒑头祷刂殿愋褪且恢碌摹?/p>
一開(kāi)始只需要打印字符串:
function print(arg: string): string { ? return arg }
后面需求變了,除了能打印字符串,還要能打印數(shù)字:
function print(arg: string | number): string | number { ? return arg }
假如需求又變了,要打印布爾值、對(duì)象、數(shù)組,甚至自定義的類型,怎么辦,寫(xiě)一串聯(lián)合類型?顯然是不可取的,用 any?那就失去了 TS 類型校驗(yàn)?zāi)芰?,淪為 JS。
function print(arg: any): any { ? return arg }
解決這個(gè)問(wèn)題的完美方法就是泛型!
print 后面加上一對(duì)尖括號(hào),里面寫(xiě)一個(gè) T,這個(gè) T 就類似是一個(gè)類型的形參。
這個(gè)類型形參可以在函數(shù)入?yún)⒗镉?,也可以在函?shù)返回值使用,甚至也可以在函數(shù)體里面的變量、函數(shù)里面用。
function print<T>(arg: T): T { ? return arg }
那么實(shí)參哪里來(lái)?用的時(shí)候傳進(jìn)來(lái)!
const res = print<number>(123)
我們還可以使用泛型來(lái)約束后端接口參數(shù)類型。
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[T]) { ? return axios.post(url, obj) } request('/book/comment', { ? id: 1, ? comment: '非常棒!' })
以上代碼對(duì)接口進(jìn)行了約束:
url 只能是 API 中定義過(guò)的,其他 url 都會(huì)提示錯(cuò)誤
接口參數(shù) obj 必須和 url 能對(duì)應(yīng)上,不能少屬性,屬性類型也不能錯(cuò)
而且調(diào)用 request 方法時(shí),也會(huì)提示 url 可以選擇哪些
如果后臺(tái)改了接口參數(shù)名,我們一眼就看出來(lái)了,都不用去找接口文檔,是不是很厲害!
泛型的例子參考了前端阿林的文章:
輕松拿下 TS 泛型
3 TS 在 Vue 中的實(shí)踐
3.1 定義組件 props 的類型
不使用 setup 語(yǔ)法糖
export default defineComponent({ ? props: { ? ? items: { ? ? ? type: Object as PropType<IResourceItem[]>, ? ? ? default() { ? ? ? ? return [] ? ? ? } ? ? }, ? ? span: { ? ? ? type: Number, ? ? ? default: 4 ? ? }, ? ? gap: { ? ? ? type: [String, Number] as PropType<string | number>, ? ? ? default: '12px' ? ? }, ? ? block: { ? ? ? type: Object as PropType<Component>, ? ? ? default: TvpBlock ? ? }, ? ? beforeClose: Function as PropType<() => boolean> ? } })
使用 setup 語(yǔ)法糖 – runtime 聲明
import { PropType, Component } from 'vue' const props = defineProps({ ? items: { ? ? type: Object as PropType<IResourceItem[]>, ? ? default() { ? ? ? return [] ? ? } ? }, ? span: { ? ? type: Number, ? ? default: 4 ? }, ? gap: { ? ? type: [String, Number] as PropType<string | number>, ? ? default: '12px' ? }, ? block: { ? ? type: Object as PropType<Component>, ? ? default: TvpBlock ? }, ? beforeClose: Function as PropType<() => boolean> })
使用 setup 語(yǔ)法糖 – type-based 聲明
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 })
IResourceItem:
interface IResourceItem { ? name: string; ? value?: string | number; ? total?: number; ? checked?: boolean; ? closable?: boolean; }
3.2 定義 emits 類型
不使用 setup 語(yǔ)法糖
export default defineComponent({ ? emits: ['change', 'update'], ? setup(props, { emit }) { ? ? emit('change') ? } })
使用 setup 語(yǔ)法糖
<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>
3.3 定義 ref 類型
默認(rèn)會(huì)自動(dòng)進(jìn)行類型推導(dǎo)
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'
兩種聲明 ref 類型的方法
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!
3.4 定義 reactive 類型
默認(rèn)會(huì)自動(dòng)進(jìn)行類型推導(dǎo)
import { reactive } from 'vue' // inferred type: { title: string } const book = reactive({ title: 'Vue 3 Guide' })
使用接口定義明確的類型
import { reactive } from 'vue' interface Book { ? title: string ? year?: number } const book: Book = reactive({ title: 'Vue 3 Guide' })
3.5 定義 computed 類型
默認(rèn)會(huì)自動(dòng)進(jìn)行類型推導(dǎo)
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('')
兩種聲明 computed 類型的方法
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 })
3.6 定義 provide/inject 類型
provide
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
inject
// 自動(dòng)推導(dǎo)為 string 類型 const foo = inject(key) // type of foo: string | undefined // 明確指定為 string 類型 const foo = inject<string>('foo') // type: string | undefined // 增加默認(rèn)值 const foo = inject<string>('foo', 'bar') // type: string // 類型斷言為 string const foo = inject('foo') as string
3.7 定義模板引用的類型
<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>
3.8 定義組件模板引用的類型
定義一個(gè) MyModal 組件
<!-- MyModal.vue --> <script setup lang="ts"> import { ref } from 'vue' const isContentShown = ref(false) const open = () => (isContentShown.value = true) defineExpose({ ? open }) </script>
在 App.vue 中引用 MyModal 組件
<!-- 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>
參考 Vue 官網(wǎng)文檔:
TypeScript with Composition API
4 JS 項(xiàng)目轉(zhuǎn) TS
還是使用 JS 的同學(xué)有福啦!為了讓大家快速用上 TS,享受 TS 的絲滑體驗(yàn),我整理了一份《JS 項(xiàng)目改造成 TS 項(xiàng)目指南》
。有了這份步驟指南,JS 項(xiàng)目轉(zhuǎn) TS 不再是難事!
我們新開(kāi)源的?TinyVue?組件庫(kù),就使用這份《JS 項(xiàng)目改造成 TS 項(xiàng)目指南》
,成功地由 JS 項(xiàng)目改造成了 TS 項(xiàng)目,悄悄地告訴大家:
TinyVue?是一套跨端、跨框架的企業(yè)級(jí) UI 組件庫(kù),支持 Vue 2 和 Vue 3,支持 PC 端和移動(dòng)端。
在內(nèi)部經(jīng)過(guò)9年持續(xù)打磨,服務(wù)于華為內(nèi)外部上千個(gè)項(xiàng)目。
目前代碼量超過(guò)
10萬(wàn)行
。
這么龐大的代碼量都能從 JS 轉(zhuǎn) TS,其他小規(guī)模的項(xiàng)目更是不在話下。
為了驗(yàn)證自己的猜想,我又在 GitHub 找到了一個(gè)6年前的 Vue2 + JS 項(xiàng)目,目前早已不再維護(hù),打算嘗試將其改造成 TS 項(xiàng)目,結(jié)果按照這份指南,1個(gè)小時(shí)不用就搞定啦啦
https://github.com/liangxiaojuan/vue-todos
這個(gè)項(xiàng)目的效果圖長(zhǎng)這樣:
我已經(jīng)提了 issue,看下作者是否同意改造成 TS,同意的話,我立馬就是一個(gè) PR 過(guò)去!
話不多說(shuō),大家有需要的,可直接拿走!
《JS 項(xiàng)目改造成 TS 項(xiàng)目指南》
JS 項(xiàng)目改造成 TS 步驟:
安裝 TS:
npm i typescript ts-loader -D
增加 TS 配置文件:
tsconfig.json
修改文件后綴名:
x.js -> x.ts
x.vue
?文件增加 lang:<script>
vite.config.js
?配置后綴名升級(jí)依賴,修改本地啟動(dòng)和構(gòu)建腳本
添加?
loader
?/?plugin
?等逐步補(bǔ)充類型聲明
tsconfig.ts
{ ? "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" ? ] }
配置文件后綴名,增加?.ts
?和?.tsx
extensions: ['.js', '.vue', '.json', '.ts', 'tsx'],
入口文件要由?main.js
?改成?main.ts
entry: { ? app: './src/main.ts' },
需要配置下 loader
{ ? test: /\.tsx?$/, ? loader: 'ts-loader', ? exclude: /node_modules/, ? options: { ? ? appendTsSuffixTo: [/\.vue$/] ? }, ? include: [resolve('src')] }
以及 plugin
const { VueLoaderPlugin } = require('vue-loader') plugins: [ ? new VueLoaderPlugin() ],
完成之后,先測(cè)試下項(xiàng)目是否能正常啟動(dòng)和構(gòu)建:npm run dev
?/?npm run build
都沒(méi)問(wèn)題之后,本次 TS 項(xiàng)目改造就完成大部分啦啦!
后續(xù)就是逐步補(bǔ)充代碼涉及到的變量和函數(shù)的類型聲明即可。
改造過(guò)程中遇到問(wèn)題歡迎留言討論,希望你也能盡快享受 TS 的絲滑開(kāi)發(fā)者體驗(yàn)!
TinyVue 招募貢獻(xiàn)者啦
如果你對(duì)我們的跨端跨框架組件庫(kù)?TinyVue?感興趣,歡迎參與到我們的開(kāi)源社區(qū)中來(lái),一起將它建設(shè)得更好!
參與 TinyVue 組件庫(kù)建設(shè),你將收獲:
直接的價(jià)值:
通過(guò)打造一個(gè)跨端、跨框架的組件庫(kù)項(xiàng)目,學(xué)習(xí)最新的?
Monorepo
?+?Vite
?+?Vue3
?+?TypeScript
?技術(shù)學(xué)習(xí)從 0 到 1 搭建一個(gè)自己的組件庫(kù)的整套流程和方法論,包括組件庫(kù)工程化、組件的設(shè)計(jì)和開(kāi)發(fā)等
為自己的簡(jiǎn)歷和職業(yè)生涯添彩,參與過(guò)優(yōu)秀的開(kāi)源項(xiàng)目,這本身就是受面試官青睞的亮點(diǎn)
結(jié)識(shí)一群優(yōu)秀的、熱愛(ài)學(xué)習(xí)、熱愛(ài)開(kāi)源的小伙伴,大家一起打造一個(gè)偉大的產(chǎn)品
長(zhǎng)遠(yuǎn)的價(jià)值:
打造個(gè)人品牌,提升個(gè)人影響力
培養(yǎng)良好的編碼習(xí)慣
獲得華為云?OpenTiny?開(kāi)源社區(qū)的榮譽(yù)&認(rèn)可和定制小禮物
成為 PMC & Committer 之后還能參與 OpenTiny 整個(gè)開(kāi)源生態(tài)的決策和長(zhǎng)遠(yuǎn)規(guī)劃,培養(yǎng)自己的管理和規(guī)劃能力 未來(lái)有更多機(jī)會(huì)和可能
往期活動(dòng)禮品及貢獻(xiàn)者的反饋:


聯(lián)系我們
如果你對(duì)我們?OpenTiny?的開(kāi)源項(xiàng)目感興趣,歡迎添加小助手微信:opentiny-official,拉你進(jìn)群,一起交流前端技術(shù),一起玩開(kāi)源。
OpenTiny 官網(wǎng):https://opentiny.design/
OpenTiny 倉(cāng)庫(kù):https://github.com/opentiny/
Vue 組件庫(kù):https://github.com/opentiny/tiny-vue(歡迎 Star ??)
Angular 組件庫(kù):https://github.com/opentiny/ng(歡迎 Star ??)