typescript快速上手的基礎知識篇
學習編程的幾個階段
1.先熟悉基礎知識,記不住沒關系,做到有個印象,知道大概什麼知識在哪一章;
2.由淺入深看示例代碼,遇到有看不懂的函數,代碼寫法先去查基礎知識,還不明白去查度娘看其他人的理解分享;
3.一直看別人的代碼,就算記住瞭。也很難真正成為你的知識,做不到舉一反三,隻是代碼的搬運工。還得多寫代碼,在別人代碼的基礎上多做改動,既能動腦思考,加深記憶,又能擴展一些新的知識點;
4.多討論代碼和多總結代碼,這也是知識和技術升華的過程。
與傳統動態弱類型語言javascript
不同,靜態類型的typescript
在執行前會先編譯成javascript
,因為它強大的type
類型系統加持,能讓我們在編寫代碼時增加更多嚴謹的限制。註意,它並不是一門全新的語言,所以並沒有增加額外的學習成本,你甚至可以將它理解為新增瞭類型封裝的javascript
,因此原先代碼邏輯怎麼寫現在還是一樣的寫,至於底層編譯的事我們無需關心。
原始數據類型
typescript
與js
一樣也有原始數據類型與對象數據類型,我們先來介紹常用的原始數據類型。
1 string
let str: string = '聽風是風'; // 支持模板字符串 const echo = '聽風是風'; let s: string = `name is ${echo}`;
2 number
let num: number = 1; let notANumber: number = NaN;
3 boolean
let bool: boolean = true;
4 任意類型any
any
用於表示任意類型,如果一個變量的類型為any
,那麼它可以被賦予任意類型的值,你可能會覺得any
應該沒啥使用場景,但恰恰相反的是,在日常趕項目趕交付期間,研發同學可能沒有太多時間去做細致化類型定義,於是any
走天下,這也是typescript
又被戲稱為anyscript
的原因。
let a: any = 4; a = "4"; a = false;
這一點也能證明typescript
所有類型都屬於any
的子類型。
5 undefined與null
let a: undefined = undefiend; let b: null = null;
默認情況下undefined
與null
這兩個類型屬於所有類型的子類型,也就是說你能將一個undefined
復制給一個number
的變量,但是開發中一般不推薦這麼做,既然我們希望typescript
為我們添加嚴格的類型判斷,那麼嚴格尊重總是有好處,你肯定也希望某種情況下undefiened
調用瞭某個number
的API
。
我們可以在tsconfig
配置中添加strictNullChecks:true
的開關,來啟用嚴格的控制判斷,這樣undefined
或者null
就隻能賦值給它們自身類型的變量,雖然這兩個類型本身用的也不多。
6 空 void
void
與any
相反,它表示沒有任意類型,比如一個函數需要返回一個字符串,你可以定義函數返回值的類型:
const fn = (): string => { return '1'; }
但假設這個函數沒有返回值,那麼就可以用void
:
const fn = (): void => { console.log(1); }
但事實上開發中如果一個函數無返回,我們也不會寫void
,因為本身也沒什麼意義,所以void
使用並不多。
另外即使打開瞭strictNullChecks
開關,我們還是能將undefined
與null
賦予給void
類型的變量,即使這也沒啥意義= =。
數組類型
array
定義支持兩種寫法,一種是類型[]
,另一種是Array<類型>
,比如:
// 元素全是number類型的數組 let arr1: number[] = [1, 2, 3]; // 元素全是string類型的數組 let arr2: Array<string> = ['1', '2', '3'];
number[]
就表示這個數組的所有元素都必須是數字,包括之後你也不能往數組中添加非數字的元素,比如:
let arr: number[] = [1, 2, 3]; arr.push('聽風是風');// error 聽風是風不是number類型
那假設數組元素種類比較多,我們可以用any
來表示數組中可以出現任意類型,比如:
let arr: any[] = [1, 2, '3']; arr.push(undefined);
接口類型(對象類型)
我們一般用接口interface
來描述對象類型(這裡的對象指{}
),比如人都有名字,性別年齡等屬性,接口即可用於對人這個類的形狀進行抽象描述,直接看個例子:
// 我們定義瞭一個叫Person的接口,推薦首字母大寫 interface Person { name: string; age: number; } let echo: Person = { name: '聽風是風', age: 28, }
我們定義瞭一個接口Person
,然後變量echo
使用瞭接口Person
,可以看到echo
和Person
的屬性形狀需要保持一致,缺屬性或者多屬性都不行:
// 少屬性 // error Property 'age' is missing in type '{ name: string; }'. let echo: Person = { name: '聽風是風', } // 多屬性 let echo: Person = { name: '聽風是風', age: 28, gender: 'male'// Person上未指定gender:string }
1 可選屬性
接口支持使用?
讓某個屬性變為可選,女性的年齡都是秘密,某種場景下我們並不能拿到age
屬性,那麼我們可以讓Person
的age
為可選,比如:
interface Person { name: string; age?: number; } // 缺少瞭age屬性,但並不會報錯 let echo: Person = { name: '西西', }
2 隻讀屬性
除瞭限定屬性可選,我們還可以通過readonly
字段來限制某個屬性隻讀,比如:
interface Person { readonly name: string; age: number; } let echo: Person = { name: '聽風是風', age: 28, } echo.name = '時間跳躍';// error 無法修改隻讀屬性
可以看到name
添加瞭隻讀,因此它在初始化之後就無法修改。
3 限制接口屬性范圍
比如上述代碼我們限制瞭名稱是字符串,也就是說任意字符串都可以,假設我們設計瞭一個組件,它的名稱屬性將決定組件如何展示,而且我們預定瞭隻支持兩種模式,那麼此時我們就可以更精確的來限制屬性范圍,比如:
interface P { name: 'wide'| 'narrow'; } let props:P = { name: 'wide' }
此時props
的name
字段隻能是wide
或者narrow
其一,輸入其它就會報錯,這樣能很好的讓組件屬性輸入符合預期。
4 額外的任意屬性
某些情況我們希望接口能添加任意屬性,那麼可以通過如下方式:
interface Person { name: string; age?: number; [propName: string]: any; } let echo: Person = { name: '聽風是風', age: 28, hobby: '吃西瓜', gender: 'male' }
[propName: string]: any
表示可以添加變量名為string
且值為any
類型,propName
的類型隻會限制額外的屬性,假設我們將其改為[propName: number]
,那麼hobby
與gender
就會報錯,而我們將其變量名改為數字則不會有問題:
interface Person { name: string; age?: number; [propName: number]: any; } let echo: Person = { name: '聽風是風', age: 28, 1: '吃西瓜', 2: 'male' }
但重點需要註意的是,假設我們定瞭額外的屬性,那麼確定屬性以及可選屬性的類型一定得是額外屬性的子類型,比如上面string
和number
都是any
的子類型,假設我們將any
改為string
,那麼age
屬性就會報錯:
interface Person { name: string; // 類型“number”的屬性“age”不能賦給字符串索引類型“string” age?: number; [propName: string]: string; } let echo: Person = { name: '聽風是風', age: 28, hobby: '吃西瓜', gender: 'male' }
假設我們不想定義any
來包含string
與number
,這裡也可以使用聯合類型string | number
來取代any
:
interface Person { name: string; age?: number; [propName: string]: string | number; } let echo: Person = { name: '聽風是風', age: 28, hobby: '吃西瓜', gender: 'male' }
函數類型
javascript
中創建函數常用有函數聲明與函數表達式兩種形式,由於函數有輸入和輸出,所以我們需要對參數以及返回結果都做限制,我們來分別介紹兩種寫法。
1 函數聲明
function sum(x: number, y: number): number { return x + y; };
比如上述的sum
函數就接受2個類型為數字的變量x,y
,並會返回它們的和,和的類型也是數字。
在限制下我們調用函數時少傳多傳,或者傳遞的參數類型不對都會有錯誤提示:
sum(1,'2');// error '2'的不是數字類型 sum(1);// error 需要2個參數但隻傳遞瞭1個 sum(1,2,3)// error 需要2個參數但傳遞瞭3個
2 函數表達式
我們將上面的代碼改為函數表達式可以是這樣:
let sum = function (x: number, y: number): number { return x + y; };
但其實這種寫法是省略瞭sum
類型的寫法,讓typescript
自己進行瞭類型推斷,啥意思呢?我們將鼠標放到sum
上你就能看到sum
自身的類型限制:
let sum: (x: number, y: number) => number let sum = function (x: number, y: number): number { return x + y; };
意思就是,我們將一個匿名函數賦值給瞭sum
,匿名函數雖然做瞭參數以及返回值的類型限定,但是我們沒對變量sum
做類型限定,sum
是什麼類型?很顯然是函數類型,所以完整的寫法應該是這樣:
let sum: (x: number, y: number) => number = function (x: number, y: number): number { return x + y; };
註意(x: number, y: number) => number
這一段是對於sum
這個變量類型的描述,表示它是一個函數類型,接受瞭哪些參數,分別是什麼類型,以及返回什麼類型。正常我們在接口中描述對象某個屬性是一個函數也是相同的寫法,比如:
interface Person { name: string; age: number; canFly: () => boolean }; let echo: Person = { name: '聽風是風', age: 28, canFly: function () { return false } }
在接口中我們對canFly
進行瞭描述,它是一個函數類型,沒有入參且返回一個佈爾值,於是在變量echo
中我們具體實現瞭這個方法,同樣沒有入參,直接返回一個佈爾值。這就相當於函數類型說明都被提到接口中統一描述瞭,而到具體實現時你不用重復再限制一次。而在上面的函數表達式中,我們要麼省略變量的限制讓typescript
自行推斷,要麼我們手動補全變量的函數類型限制,其實就是這個意思。
當然,如果你覺得自己補全看著函數太長瞭,我們也能將函數變量這一塊的描述交給接口,這樣看著就相對簡潔一點:
// 將函數變量的約束抽離出來給接口來做 interface MySum { (x: number, y: number): number } let sum: MySum = function (x: number, y: number): number { return x + y; };
3 可選參數
函數同樣支持使用?
來表示某個參數可選:
function sum(x: number, y?: number): number { return x + y; }; sum(1);
4 參數默認值
有默認值的參數在typescript
中會被默認識別為可選參數,畢竟有默認值傳不傳遞都可以:
function sum(x: number, y: number = 1): number { return x + y; }; sum(1);
5 …rest參數
在es6
中我們可以用...rest
來表示函數剩餘參數,這也巧妙解決瞭arguments
是類數組無法使用數組api
的問題,因為在函數內rest
就是一個數組,因此我們可以用數組類型來描述rest
,比如:
function fn(a: number, ...rest: any[]) { rest.forEach((item) => console.log(item)) } fn(1, 2, 3, 4, 5);
聯合類型
很多時候,我們可能需要讓一個變量支持數字以及字符串等多種類型,這時候就需要使用聯合類型,它使用符號|
表示,比如:
// echo的值可以是數字或者字符串 let echo: number | string = 'echo'; echo = 1;
以上代碼中的echo
可以被賦值為任意的字符串或數字類型的值。但需要註意的是,當我們未給echo
賦予準確的值,但需要訪問某個屬性或api
,此時隻能訪問聯合類型共有的屬性或者方法,比如:
let echo: number | string; // 數字和字符串都支持toString方法 echo.toString(); // 報錯,Property 'toFixed' does not exist on type 'string'. echo.toFixed(1);
但假設我們給echo
賦予具體的值,此時聯合類型同樣會走類型推斷,從而讓我們能正確使用對應類型的api
,比如:
let echo: number | string; // 此時被推斷成字符串,因此能調用字符串的api echo = '聽風是風'; echo.length;// 4 // 此時被推斷成數字,因此能調用數字的api echo = 1.021; echo.toFixed(2); // '1.02'
類型推斷
首先類型推斷屬於typescript
的一個概念,相當於typescript
底層自動會幫我們做的一件事,大傢作為瞭解就好。
正常來說我們定義字符串是這樣,我們明確標明瞭str
的類型,以及符合預期的修改str
的值:
let str: string = 'echo'; str = '聽風是風';
但假設我們不去定義一個變量的類型,但賦予瞭這個變量一個明確的值,比如一個字符串,再修改值為數字時,你會發現報錯瞭:
let str = 'echo'; str = 1; //Type '1' is not assignable to type 'string'.
這是因為typescript
會根據我們最初賦予的值,嘗試去推斷這個變量的類型,所以即便我們沒指定具體類型,後續也不能隨意修改值的類型,這就是所謂的類型推斷瞭。
上述代碼等價於:
let str: string = 'echo'; str = 1; //Type '1' is not assignable to type 'string'.
也就是說,隻要你定義的文件是.ts
,就別想著在ts
文件不定義類型然後隨意賦值,這對於ts
而言肯定是不允許的。除瞭上文提到的幾個不常用的類型,日常開發中我們還是希望能明確標明變量類型。
還有一種比較特殊,我指定以瞭一個變量但沒賦值,這時候因為沒具體的值,所以typescript
會將這個變量推斷成any
類型,因此我們可以隨意修改這個變量的值,比如:
let echo; echo = '時間跳躍'; echo = 1; // 等同於 let echo: any; echo = '時間跳躍'; echo = 1;
類型斷言
如果說類型推斷是typescript
自動做的類型判斷,那麼類型斷言就是我們人為手動的來指定一個值的類型,它支持兩種寫法:
// <類型>變量名 <string>echo // 變量名 as 類型 echo as string
需要註意的是,在tsx
文件中隻支持as
這種寫法,保險起見統一使用as
更穩。
為什麼會有類型斷言的使用場景?我們來看幾個例子你就明白瞭。
1 聯合類型斷言場景
我們前面說瞭,當一個變量沒具體賦值,且有聯合類型時,它隻能使用聯合類型共有的屬性或方法,那假設我們現在封裝瞭一個方法:
function fn(s: string | number): number { // 報錯 Property 'length' does not exist on type 'number'. return s.length; };
我們假定參數s
可能是字符串或者數字兩種類型,但是你很清楚這個方法隻會接受到字符串,那我們就可以手動指定s
的類型,比如:
function fn(s: string | number): number { return (s as string).length; };
類型斷言的目的就是我們開發者主動的告訴typescipt
,我現在很清楚這個變量此時的類型是什麼,從而讓typescript
不報錯,但上述編碼編譯成js
後其實也隻是一個普通的返回s.length
的函數,假設參數依舊傳遞瞭一個數字進來,那麼還是無法避免報錯的尷尬,所以使用類型斷言時一定得謹慎,它隻是繞過typescirpt
報錯,並沒有從根源上解決代碼兼容問題。
上述代碼通過if
來限制執行,你會發現這樣實現其實更穩:
function fn(s: string | number): number { let length = 0; // 我們添加瞭判斷,也相當於瞭人為對類型做瞭判斷,因此也不會報錯 if (typeof s === 'string') { length = s.length; }; return length; };
2 父子類斷言場景
ES6
支持類的定義與繼承,而當類具有繼承關系時,類型斷言也會起到作用,比如:
class P { } class P1 extends P { a: number = 1; } class P2 extends P { b: number = 2; } const fn = (s: P): boolean => { // 報錯 Property 'a' does not exist on type 'P'. if (typeof s.a === 'number') { return true; } return false; }
這裡我們定義瞭一個父類P
,基於P
繼承得到瞭P1 P2
兩個類,且兩個類都有屬於自己的實例屬性,現在我們定義瞭一個比較通用的檢測P
類屬性的方法,考慮到公用型,所以類型定義我們使用P
,但在內部實現中,typescript
會告訴你P
上並沒有屬性a
。這時候我們同樣可以利用斷言將類型精確到子類P1
,如下:
const fn = (s: P): boolean => { if (typeof (s as P1).a === 'number') { return true; } return false; }
當然這也隻是繞過瞭typescript
的檢測,假設我們傳遞瞭一個P2
實例進來,你會發現代碼會報錯,這並沒有解決根本問題。所以更好的做法還是從邏輯層間提升代碼穩定性,比如:
const fn = (s: P): boolean => { if (s instanceof P1) { return true; } return false; }
3 將任意類型斷言為any
javascript
中存在很多原生對象,這些對象一開始就沒被添加類型,比如我們希望在全局對象window
上添加屬性就會報錯:
window.echo = 1;// error window上不存在echo的類型
這時候我們可以手動將window
斷言為any
以解決修改屬性以及添加屬性的問題:
(window as any).echo = 1;
我們知道window
上默認自帶一些屬性,比如name
字段默認是一個空字符,因此在typescript
中name
也默認被推斷成瞭string
類型,比如我們想將window.name
修改為數字默認會報錯:
window.name = 1;// error 不能將1賦予給字符串類型的name
而斷言成any
可以讓我們任意修改window
上的屬性類型,這很方便但也有一定風險,在實際開發中請謹慎對待。
4 將any斷言成精確的類型
在舊有代碼遷移ts
或者維護不規范的ts
代碼時,我們可能會遇到因為趕項目趕時間而定義比較隨意的any
類型,而你瞭解瞭這段代碼其實知道類型定義可以更為精確,重寫重構代碼寫出完全規范的代碼之外,你能通過斷言對於模糊類型進行補救,比如:
interface UserInfo { name: string; age: number; } function getUserInfo(): any { return { name: '聽風是風', age:28 } }; const user = getUserInfo() as UserInfo;
這樣user
後續的代碼就能清楚知道這個對象是什麼類型,對應讓代碼編寫更嚴謹。
5 類型斷言的限制
斷言雖然在某些場景很好用,但它也得滿足一些斷言場景,畢竟我們總不能將貓的類型斷言成魚的類型,這就不符合規范瞭。那麼滿足什麼條件才能斷言呢?先說結論,當類型A兼容瞭類型B,或者B兼容瞭A,那麼A可以斷言成B,B也能斷言成A。
什麼意思?我們在上文提到,所有的類型都是any
的子類型,也就是說any
兼容瞭其它所有類型,比如string
類型,因此我們可以將any as string
,也能將string as any
,這都是可以的。
我們再來看個例子:
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;
我們定義瞭Person
與User
兩個接口,比較有趣的事,我們將已定義瞭User
類的變量echo
賦值給xixi
,而xixi
的類Person
並沒有age
屬性,但並不會報錯,而假設我們將代碼改為如下這樣,就會報錯:
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中未定義 };
為啥上面正常,下面這段代碼就報錯瞭,區別在哪?區別就在於上面的代碼的echo
提前定義好瞭User
類型,而下面的賦值隻是一個單純的對象,是一個數據,它無類型。
那為什麼上面不報錯呢?其實說到底還是底層類型斷言幫我們做瞭處理,上面的Person
與User
類型的關系你可以理解為:
interface Person { name: string; } // User繼承瞭Person接口,並額外添加瞭age interface User extends Person{ age: number; }
因此接口Person
兼容瞭User
(有相同屬性,屬性少的兼容屬性多的)。還記得上文父子類斷言場景中,我們將父類斷言成子類的操作嗎?你沒發現參數處我們用瞭屬性更少的父類P
,但事實上傳遞進來的參數可能是接口P1
或者P2
的對象,它們的屬性都比P
接口定義的屬性要多,但是參數這裡並不會報錯,而且在函數內我們還能將P
斷言為P1
,本質原因也是P
兼容瞭P1 P2
。
其實總結上面聊到的幾個使用場景:
- 聯合類型
string | number
斷言為string
,兩者存在兼容關系。 - 父類斷言成子類場景,兩者同樣存在兼容關系。
any
斷言成任意,任意類型斷言成any
,本質上也是兼容關系。
因此,隻要兩個類型存在兼容關系,不管誰兼容誰,都能相互斷言成對方的類型。
另外,我們在前面說,貓類型不能斷言成魚類型,但現在你會發現一個很有趣的事情,貓類型和any
是包含關系,而any
和魚類型同樣是包含關系,那我能不能cat as any as fish
雙重斷言直接實現跨物種進化呢?很明顯通過這種做法,我們能實現類的任意斷言,但typescript
中一般不推薦這麼做,因為大概率會導致類型錯誤….
5 類型斷言與類型聲明
我們在將any
斷言成其它類型講解中,為瞭完善類型定義模糊的代碼,我們給瞭一個例子,其實它還有其它的修復方法,比如不全函數的返回值的類型:
interface UserInfo { name: string; age: number; } function getUserInfo(): UserInfo { return { name: '聽風是風', age:28 } }; const user = getUserInfo();
除此之外,我們還能補全user
的類型聲明,達到相同的效果,比如:
interface UserInfo { name: string; age: number; } function getUserInfo(): any { return { name: '聽風是風', age: 28 } }; const user: UserInfo = getUserInfo();
單看類型斷言和類型聲明的補全的,你會發現後續使用user
完全一致,那這兩者有啥區別嗎?我們再來看個例子:
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
上述代碼很明顯Person
兼容瞭User
,因此變量echo
我們能使用斷言將其類型由Person
轉為User
後再賦值給xixi
,之後xixi
就能賦予age
屬性瞭。
但假設上述代碼我們通過類型聲明來做,如下,你會發現報錯瞭:
interface Person { name: string; } interface User { name: string; age: number; } let echo: Person = { name: '聽風是風' } // error Person缺少age屬性 let xixi: User = echo;
為什麼報錯?很明顯echo
的Person
類沒有age
屬性,而User
需要age
屬性,回到斷言限制的第一個例子,對比下你會發現,我們將一個屬性更多的類賦值給一個屬性更少的類可以(後者兼容前者),反過來則會報錯。
結合類型斷言,你會發現類型聲明的限制比類型斷言要嚴格的多,大致我們可以總結為:
- 若
A
需要斷言成B
,隻需要A
兼容B
或者B
兼容A
都可以 - 若將
A
賦值給B
,那麼一定是B
兼容A
才行(被賦值的類型屬性比作為值的類型屬性要少才行)。
以上就是兩者的區別。
內置對象
前文我們提到,在js
中其實存在很多內置對象,比如window、Date、RegExp
等等,你有沒有想過,假設我現在聲明瞭一個正則表達式,那我應該添加什麼類型?
其實在typescript
底層已經幫我們封裝好瞭這些類型,我們可以直接使用,看部分例子:
let a: Boolean = new Boolean(1); let b: Error = new Error('報錯啦'); let c: Date = new Date(); let d: RegExp = /'聽風是風'/;
可以看到通過new
一個構造器得到的實例,它們的類型都是首字母大寫,正則比較特殊,不管是對象聲明還是new
它本身就隻支持RegExp
。
再比如js
中內置瞭一些DOM
以及BOM
對象,比如類數組NodeList
,事件對象Event
等,這些也能直接用於類型定義,比如:
let div:NodeList = document.querySelectorAll('.div');
更多內置對象類型查閱MDN
,這裡就不一一細說瞭。
總結
那麼到這裡,我們已經快速過完瞭typescript
基礎內容,準備來說這些知識已經足以支撐看懂項目中大部分ts
代碼瞭,畢竟ts
也沒有太多的額外知識,更多是對於js
類型的限制,後續會再利用進階篇補全剩下概念,那麼到這裡全文結束。
到此這篇關於typescript快速上手的基礎知識篇的文章就介紹到這瞭,更多相關typescript基礎知識快速上手內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- TypeScript保姆級基礎教程
- TypeScript基本類型之typeof和keyof詳解
- TypeScript基礎類型介紹
- typescript返回值類型和參數類型的具體使用
- TypeScript定義接口(interface)案例教程