下一代前端開發(fā)語(yǔ)言 TypeScript從零到實(shí)踐(6)

泛型

軟件工程中,我們不僅要?jiǎng)?chuàng)建定義良好且一致的 API,同時(shí)也要考慮可重用性。 組件不僅能夠支持當(dāng)前的數(shù)據(jù)類型,同時(shí)也能支持未來(lái)的數(shù)據(jù)類型,這在創(chuàng)建大型系統(tǒng)時(shí)為你提供了十分靈活的功能。

在像 C# 和 Java 這樣的語(yǔ)言中,可以使用泛型來(lái)創(chuàng)建可重用的組件,一個(gè)組件可以支持多種類型的數(shù)據(jù)。 這樣用戶就可以以自己的數(shù)據(jù)類型來(lái)使用組件。

基礎(chǔ)示例

下面來(lái)創(chuàng)建第一個(gè)使用泛型的例子:identity 函數(shù)。 這個(gè)函數(shù)會(huì)返回任何傳入它的值。 你可以把這個(gè)函數(shù)當(dāng)成是 echo 命令。

不用泛型的話,這個(gè)函數(shù)可能是下面這樣:

function identity(arg: number): number {
  return arg
}

或者,我們使用 any 類型來(lái)定義函數(shù):

function identity(arg: any): any {
  return arg
}

使用 any 類型會(huì)導(dǎo)致這個(gè)函數(shù)可以接收任何類型的 arg 參數(shù),但是這樣就丟失了一些信息:傳入的類型與返回的類型應(yīng)該是相同的。如果我們傳入一個(gè)數(shù)字,我們只知道任何類型的值都有可能被返回。

因此,我們需要一種方法使返回值的類型與傳入?yún)?shù)的類型是相同的。這里,我們使用了類型變量,它是一種特殊的變量,只用于表示類型而不是值。

function identity<T>(arg: T): T {
  return arg
}

我們給 identity 添加了類型變量 TT 幫助我們捕獲用戶傳入的類型(比如:number),之后我們就可以使用這個(gè)類型。 之后我們?cè)俅问褂昧?T 當(dāng)做返回值類型?,F(xiàn)在我們可以知道參數(shù)類型與返回值類型是相同的了。這允許我們跟蹤函數(shù)里使用的類型的信息。

我們把這個(gè)版本的 identity 函數(shù)叫做泛型,因?yàn)樗梢赃m用于多個(gè)類型。 不同于使用 any,它不會(huì)丟失信息,像第一個(gè)例子那像保持準(zhǔn)確性,傳入數(shù)值類型并返回?cái)?shù)值類型。

我們定義了泛型函數(shù)后,可以用兩種方法使用。 第一種是,傳入所有的參數(shù),包含類型參數(shù):

let output = identity<string>('myString')

這里我們明確的指定了 Tstring 類型,并做為一個(gè)參數(shù)傳給函數(shù),使用了 <> 括起來(lái)而不是 ()。

第二種方法更普遍。利用了類型推論 -- 即編譯器會(huì)根據(jù)傳入的參數(shù)自動(dòng)地幫助我們確定 T 的類型:

let output = identity('myString')

注意我們沒必要使用尖括號(hào)(<>)來(lái)明確地傳入類型;編譯器可以查看 myString 的值,然后把 T 設(shè)置為它的類型。 類型推論幫助我們保持代碼精簡(jiǎn)和高可讀性。如果編譯器不能夠自動(dòng)地推斷出類型的話,只能像上面那樣明確的傳入 T 的類型,在一些復(fù)雜的情況下,這是可能出現(xiàn)的。

使用泛型變量

使用泛型創(chuàng)建像 identity 這樣的泛型函數(shù)時(shí),編譯器要求你在函數(shù)體必須正確的使用這個(gè)通用的類型。 換句話說(shuō),你必須把這些參數(shù)當(dāng)做是任意或所有類型。

看下之前 identity 例子:

function identity<T>(arg: T): T {
  return arg
}

如果我們想打印出 arg 的長(zhǎng)度。 我們很可能會(huì)這樣做:

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length)
  return arg
}

如果這么做,編譯器會(huì)報(bào)錯(cuò)說(shuō)我們使用了 arg.length 屬性,但是沒有地方指明 arg 具有這個(gè)屬性。記住,這些類型變量代表的是任意類型,所以使用這個(gè)函數(shù)的人可能傳入的是個(gè)數(shù)字,而數(shù)字是沒有 .length 屬性的。

現(xiàn)在假設(shè)我們想操作 T 類型的數(shù)組而不直接是 T。由于我們操作的是數(shù)組,所以 .length 屬性是應(yīng)該存在的。我們可以像創(chuàng)建其它數(shù)組一樣創(chuàng)建這個(gè)數(shù)組:

function loggingIdentity<T>(arg: T[]): T[] {
  console.log(arg.length)
  return arg
}

你可以這樣理解 loggingIdentity 的類型:泛型函數(shù) loggingIdentity,接收類型參數(shù) T 和參數(shù) arg,它是個(gè)元素類型是 T 的數(shù)組,并返回元素類型是T 的數(shù)組。 如果我們傳入數(shù)字?jǐn)?shù)組,將返回一個(gè)數(shù)字?jǐn)?shù)組,因?yàn)榇藭r(shí) T 的的類型為 number。 這可以讓我們把泛型變量 T 當(dāng)做類型的一部分使用,而不是整個(gè)類型,增加了靈活性。

泛型類型

上一節(jié),我們創(chuàng)建了 identity 通用函數(shù),可以適用于不同的類型。 在這節(jié),我們研究一下函數(shù)本身的類型,以及如何創(chuàng)建泛型接口。

泛型函數(shù)的類型與非泛型函數(shù)的類型沒什么不同,只是有一個(gè)類型參數(shù)在最前面,像函數(shù)聲明一樣:

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: <T>(arg: T) => T = identity

我們也可以使用不同的泛型參數(shù)名,只要在數(shù)量上和使用方式上能對(duì)應(yīng)上就可以。

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: <U>(arg: U) => U = identity

我們還可以使用帶有調(diào)用簽名的對(duì)象字面量來(lái)定義泛型函數(shù):

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: {<T>(arg: T): T} = identity

這引導(dǎo)我們?nèi)懙谝粋€(gè)泛型接口了。我們把上面例子里的對(duì)象字面量拿出來(lái)做為一個(gè)接口:

interface GenericIdentityFn {
  <T>(arg: T): T
}

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: GenericIdentityFn = identity

我們甚至可以把泛型參數(shù)當(dāng)作整個(gè)接口的一個(gè)參數(shù)。 這樣我們就能清楚的知道使用的具體是哪個(gè)泛型類型(比如: Dictionary<string> 而不只是Dictionary)。這樣接口里的其它成員也能知道這個(gè)參數(shù)的類型了。

interface GenericIdentityFn<T> {
  (arg: T): T
}

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: GenericIdentityFn<number> = identity

注意,我們的示例做了少許改動(dòng)。 不再描述泛型函數(shù),而是把非泛型函數(shù)簽名作為泛型類型一部分。 當(dāng)我們使用 GenericIdentityFn 的時(shí)候,還得傳入一個(gè)類型參數(shù)來(lái)指定泛型類型(這里是:number),鎖定了之后代碼里使用的類型。對(duì)于描述哪部分類型屬于泛型部分來(lái)說(shuō),理解何時(shí)把參數(shù)放在調(diào)用簽名里和何時(shí)放在接口上是很有幫助的。

除了泛型接口,我們還可以創(chuàng)建泛型類。 注意,無(wú)法創(chuàng)建泛型枚舉和泛型命名空間。

泛型類

泛型類看上去與泛型接口差不多。 泛型類使用( <>)括起泛型類型,跟在類名后面。

class GenericNumber<T> {
  zeroValue: T
  add: (x: T, y: T) => T
}

let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function(x, y) {
  return x + y 
}

GenericNumber 類的使用是十分直觀的,并且你可能已經(jīng)注意到了,沒有什么去限制它只能使用 number 類型。 也可以使用字符串或其它更復(fù)雜的類型。

let stringNumeric = new GenericNumber<string>()
stringNumeric.zeroValue = ''
stringNumeric.add = function(x, y) { 
  return x + y
}

console.log(stringNumeric.add(stringNumeric.zeroValue, 'test'))

與接口一樣,直接把泛型類型放在類后面,可以幫助我們確認(rèn)類的所有屬性都在使用相同的類型。

我們?cè)?a href="/chapter2/class" target="_blank">類那節(jié)說(shuō)過(guò),類有兩部分:靜態(tài)部分和實(shí)例部分。 泛型類指的是實(shí)例部分的類型,所以類的靜態(tài)屬性不能使用這個(gè)泛型類型。

泛型約束

我們有時(shí)候想操作某類型的一組值,并且我們知道這組值具有什么樣的屬性。在 loggingIdentity 例子中,我們想訪問 arglength 屬性,但是編譯器并不能證明每種類型都有 length 屬性,所以就報(bào)錯(cuò)了。

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length)
  return arg
}

相比于操作 any 所有類型,我們想要限制函數(shù)去處理任意帶有 .length 屬性的所有類型。 只要傳入的類型有這個(gè)屬性,我們就允許,就是說(shuō)至少包含這一屬性。為此,我們需要列出對(duì)于 T 的約束要求。

我們定義一個(gè)接口來(lái)描述約束條件,創(chuàng)建一個(gè)包含 .length 屬性的接口,使用這個(gè)接口和 extends 關(guān)鍵字來(lái)實(shí)現(xiàn)約束:

interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length) // OK
  return arg
}

現(xiàn)在這個(gè)泛型函數(shù)被定義了約束,因此它不再是適用于任意類型:

loggingIdentity(3);  // Error

我們需要傳入符合約束類型的值,必須包含必須的屬性:

loggingIdentity({length: 10, value: 3}) // OK

在泛型約束中使用類型參數(shù)

你可以聲明一個(gè)類型參數(shù),且它被另一個(gè)類型參數(shù)所約束。 比如,現(xiàn)在我們想要用屬性名從對(duì)象里獲取這個(gè)屬性。 并且我們想要確保這個(gè)屬性存在于對(duì)象 obj 上,因此我們需要在這兩個(gè)類型之間使用約束。

function getProperty<T, K extends keyof T> (obj: T, key: K ) {
  return obj[key]
}

let x = {a: 1, b: 2, c: 3, d: 4}

getProperty(x, 'a') // okay
getProperty(x, 'm') // error

高級(jí)類型

交叉類型

交叉類型是將多個(gè)類型合并為一個(gè)類型。 這讓我們可以把現(xiàn)有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。 例如,Person & Loggable 同時(shí)是 PersonLoggable。 就是說(shuō)這個(gè)類型的對(duì)象同時(shí)擁有了這兩種類型的成員。

我們大多是在混入(mixins)或其它不適合典型面向?qū)ο竽P偷牡胤娇吹浇徊骖愋偷氖褂谩?(在 JavaScript 里發(fā)生這種情況的場(chǎng)合很多?。?下面是如何創(chuàng)建混入的一個(gè)簡(jiǎn)單例子:

function extend<T, U> (first: T, second: U): T & U {
  let result = {} as T & U
  for (let id in first) {
    result[id] = first[id] as any
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      result[id] = second[id] as any
    }
  }
  return result
}

class Person {
  constructor (public name: string) {
  }
}

interface Loggable {
  log (): void
}

class ConsoleLogger implements Loggable {
  log () {
    // ...
  }
}

var jim = extend(new Person('Jim'), new ConsoleLogger())
var n = jim.name
jim.log()

聯(lián)合類型

聯(lián)合類型與交叉類型很有關(guān)聯(lián),但是使用上卻完全不同。 偶爾你會(huì)遇到這種情況,一個(gè)代碼庫(kù)希望傳入 numberstring 類型的參數(shù)。 例如下面的函數(shù):

function padLeft(value: string, padding: any) {
  if (typeof padding === 'number') {
    return Array(padding + 1).join(' ') + value
  }
  if (typeof padding === 'string') {
    return padding + value
  }
  throw new Error(`Expected string or number, got '${padding}'.`)
}

padLeft('Hello world', 4) // returns "    Hello world"

padLeft 存在一個(gè)問題,padding 參數(shù)的類型指定成了 any。 這就是說(shuō)我們可以傳入一個(gè)既不是 number 也不是 string 類型的參數(shù),但是 TypeScript 卻不報(bào)錯(cuò)。

let indentedString = padLeft('Hello world', true) // 編譯階段通過(guò),運(yùn)行時(shí)報(bào)錯(cuò)

為了解決這個(gè)問題,我們可以使用 聯(lián)合類型做為 padding 的參數(shù):

function padLeft(value: string, padding: string | number) {
  // ...
}

let indentedString = padLeft('Hello world', true) // 編譯階段報(bào)錯(cuò)

聯(lián)合類型表示一個(gè)值可以是幾種類型之一。我們用豎線(|)分隔每個(gè)類型,所以 number | string 表示一個(gè)值可以是 numberstring。

如果一個(gè)值是聯(lián)合類型,我們只能訪問此聯(lián)合類型的所有類型里共有的成員。

interface Bird {
  fly()
  layEggs()
}

interface Fish {
  swim()
  layEggs()
}

function getSmallPet(): Fish | Bird {
  // ...
}

let pet = getSmallPet()
pet.layEggs() // okay
pet.swim()    // error

這里的聯(lián)合類型可能有點(diǎn)復(fù)雜:如果一個(gè)值的類型是 A | B,我們能夠確定的是它包含了 AB 中共有的成員。這個(gè)例子里,Fish 具有一個(gè) swim 方法,我們不能確定一個(gè) Bird | Fish 類型的變量是否有 swim方法。 如果變量在運(yùn)行時(shí)是 Bird 類型,那么調(diào)用 pet.swim() 就出錯(cuò)了。

類型保護(hù)

聯(lián)合類型適合于那些值可以為不同類型的情況。 但當(dāng)我們想確切地了解是否為 Fish 或者是 Bird 時(shí)怎么辦? JavaScript 里常用來(lái)區(qū)分這 2 個(gè)可能值的方法是檢查成員是否存在。如之前提及的,我們只能訪問聯(lián)合類型中共同擁有的成員。

let pet = getSmallPet()

// 每一個(gè)成員訪問都會(huì)報(bào)錯(cuò)
if (pet.swim) {
  pet.swim()
} else if (pet.fly) {
  pet.fly()
}

為了讓這段代碼工作,我們要使用類型斷言:

let pet = getSmallPet()

if ((pet as Fish).swim) {
  (pet as Fish).swim()
} else {
  (pet as Bird).fly()
}

用戶自定義的類型保護(hù)

這里可以注意到我們不得不多次使用類型斷言。如果我們一旦檢查過(guò)類型,就能在之后的每個(gè)分支里清楚地知道 pet 的類型的話就好了。

TypeScript 里的類型保護(hù)機(jī)制讓它成為了現(xiàn)實(shí)。 類型保護(hù)就是一些表達(dá)式,它們會(huì)在運(yùn)行時(shí)檢查以確保在某個(gè)作用域里的類型。定義一個(gè)類型保護(hù),我們只要簡(jiǎn)單地定義一個(gè)函數(shù),它的返回值是一個(gè)類型謂詞

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined
}

在這個(gè)例子里,pet is Fish 就是類型謂詞。謂詞為 parameterName is Type 這種形式, parameterName 必須是來(lái)自于當(dāng)前函數(shù)簽名里的一個(gè)參數(shù)名。

每當(dāng)使用一些變量調(diào)用 isFish 時(shí),TypeScript 會(huì)將變量縮減為那個(gè)具體的類型。

if (isFish(pet)) {
  pet.swim()
}
else {
  pet.fly()
}

注意 TypeScript 不僅知道在 if 分支里 petFish 類型;它還清楚在 else 分支里,一定不是 Fish類型而是 Bird 類型。

typeof 類型保護(hù)

現(xiàn)在我們回過(guò)頭來(lái)看看怎么使用聯(lián)合類型書寫 padLeft 代碼。我們可以像下面這樣利用類型斷言來(lái)寫:

function isNumber (x: any):x is string {
  return typeof x === 'number'
}

function isString (x: any): x is string {
  return typeof x === 'string'
}

function padLeft (value: string, padding: string | number) {
  if (isNumber(padding)) {
    return Array(padding + 1).join(' ') + value
  }
  if (isString(padding)) {
    return padding + value
  }
  throw new Error(`Expected string or number, got '${padding}'.`)
}

然而,你必須要定義一個(gè)函數(shù)來(lái)判斷類型是否是原始類型,但這并不必要。其實(shí)我們不必將 typeof x === 'number'抽象成一個(gè)函數(shù),因?yàn)?TypeScript 可以將它識(shí)別為一個(gè)類型保護(hù)。 也就是說(shuō)我們可以直接在代碼里檢查類型了。

function padLeft (value: string, padding: string | number) {
  if (typeof padding === 'number') {
    return Array(padding + 1).join(' ') + value
  }
  if (typeof padding === 'string') {
    return padding + value
  }
  throw new Error(`Expected string or number, got '${padding}'.`)
}

這些 typeof 類型保護(hù)只有兩種形式能被識(shí)別:typeof v === "typename"typeof v !== "typename", "typename"必須是 "number", "string","boolean""symbol"。 但是 TypeScript 并不會(huì)阻止你與其它字符串比較,只是 TypeScript 不會(huì)把那些表達(dá)式識(shí)別為類型保護(hù)。

instanceof 類型保護(hù)

如果你已經(jīng)閱讀了 typeof 類型保護(hù)并且對(duì) JavaScript 里的 instanceof 操作符熟悉的話,你可能已經(jīng)猜到了這節(jié)要講的內(nèi)容。

instanceof 類型保護(hù)是通過(guò)構(gòu)造函數(shù)來(lái)細(xì)化類型的一種方式。我們把之前的例子做一個(gè)小小的改造:

class Bird {
  fly () {
    console.log('bird fly')
  }

  layEggs () {
    console.log('bird lay eggs')
  }
}

class Fish {
  swim () {
    console.log('fish swim')
  }

  layEggs () {
    console.log('fish lay eggs')
  }
}

function getRandomPet () {
  return Math.random() > 0.5 ? new Bird() : new Fish()
}

let pet = getRandomPet()

if (pet instanceof Bird) {
  pet.fly()
}
if (pet instanceof Fish) {
  pet.swim()
}

可以為 null 的類型

TypeScript 具有兩種特殊的類型,nullundefined,它們分別具有值 nullundefined。我們?cè)?a href="/chapter2/type" target="_blank">基礎(chǔ)類型一節(jié)里已經(jīng)做過(guò)簡(jiǎn)要說(shuō)明。 默認(rèn)情況下,類型檢查器認(rèn)為 nullundefined 可以賦值給任何類型。 nullundefined 是所有其它類型的一個(gè)有效值。 這也意味著,你阻止不了將它們賦值給其它類型,就算是你想要阻止這種情況也不行。null的發(fā)明者,Tony Hoare,稱它為價(jià)值億萬(wàn)美金的錯(cuò)誤。

--strictNullChecks 標(biāo)記可以解決此錯(cuò)誤:當(dāng)你聲明一個(gè)變量時(shí),它不會(huì)自動(dòng)地包含 nullundefined。 你可以使用聯(lián)合類型明確的包含它們:

let s = 'foo'
s = null // 錯(cuò)誤, 'null'不能賦值給'string'
let sn: string | null = 'bar'
sn = null // 可以

sn = undefined // error, 'undefined'不能賦值給'string | null'

注意,按照 JavaScript 的語(yǔ)義,TypeScript 會(huì)把 nullundefined 區(qū)別對(duì)待。string | null,string | undefinedstring | undefined | null 是不同的類型。

可選參數(shù)和可選屬性

使用了 --strictNullChecks,可選參數(shù)會(huì)被自動(dòng)地加上 | undefined:

function f(x: number, y?: number) {
  return x + (y || 0)
}
f(1, 2)
f(1)
f(1, undefined)
f(1, null) // error, 'null' 不能賦值給 'number | undefined'

可選屬性也會(huì)有同樣的處理:

class C {
  a: number
  b?: number
}
let c = new C()
c.a = 12
c.a = undefined // error, 'undefined' 不能賦值給 'number'
c.b = 13
c.b = undefined // ok
c.b = null // error, 'null' 不能賦值給 'number | undefined'

類型保護(hù)和類型斷言

由于可以為 null 的類型能和其它類型定義為聯(lián)合類型,那么你需要使用類型保護(hù)來(lái)去除 null。幸運(yùn)地是這與在 JavaScript 里寫的代碼一致:

function f(sn: string | null): string {
  if (sn === null) {
    return 'default'
  } else {
    return sn
  }
}

這里很明顯地去除了 null,你也可以使用短路運(yùn)算符:

function f(sn: string | null): string {
  return sn || 'default'
}

如果編譯器不能夠去除 nullundefined,你可以使用類型斷言手動(dòng)去除。語(yǔ)法是添加 ! 后綴: identifier!identifier 的類型里去除了 nullundefined

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet // error, 'name' 可能為 null
  }
  name = name || 'Bob'
  return postfix('great')
}

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet // ok
  }
  name = name || 'Bob'
  return postfix('great')
}

broken(null)

本例使用了嵌套函數(shù),因?yàn)榫幾g器無(wú)法去除嵌套函數(shù)的 null(除非是立即調(diào)用的函數(shù)表達(dá)式)。因?yàn)樗鼰o(wú)法跟蹤所有對(duì)嵌套函數(shù)的調(diào)用,尤其是你將內(nèi)層函數(shù)做為外層函數(shù)的返回值。如果無(wú)法知道函數(shù)在哪里被調(diào)用,就無(wú)法知道調(diào)用時(shí) name 的類型。

字符串字面量類型

字符串字面量類型允許你指定字符串必須具有的確切值。在實(shí)際應(yīng)用中,字符串字面量類型可以與聯(lián)合類型,類型保護(hù)很好的配合。通過(guò)結(jié)合使用這些特性,你可以實(shí)現(xiàn)類似枚舉類型的字符串。

type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'

class UIElement {
  animate (dx: number, dy: number, easing: Easing) {
    if (easing === 'ease-in') {
      // ...
    } else if (easing === 'ease-out') {
    } else if (easing === 'ease-in-out') {
    } else {
      // error! 不能傳入 null 或者 undefined.
    }
  }
}

let button = new UIElement()
button.animate(0, 0, 'ease-in')
button.animate(0, 0, 'uneasy') // error

你只能從三種允許的字符中選擇其一來(lái)做為參數(shù)傳遞,傳入其它值則會(huì)產(chǎn)生錯(cuò)誤。

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

總結(jié)

那么到這里,我們的 TypeScript 常用語(yǔ)法學(xué)習(xí)就告一段落了,當(dāng)然 TypeScript 還有其他的語(yǔ)法我們并沒有講,我們只是講了 TypeScript 的一些常用語(yǔ)法,你們把這些知識(shí)學(xué)會(huì)已經(jīng)足以開發(fā)一般的應(yīng)用了。如果你在使用 TypeScript 開發(fā)項(xiàng)目中遇到了其他的 TypeScript 語(yǔ)法知識(shí),你可以通過(guò) TypeScript 的官網(wǎng)文檔學(xué)習(xí)。因?yàn)閷W(xué)基礎(chǔ)最好的方法還是去閱讀它的官網(wǎng)文檔,敲上面的小例子。其實(shí)我們課程的基礎(chǔ)知識(shí)結(jié)構(gòu)也是大部分參考了官網(wǎng)文檔,要記住學(xué)習(xí)一門技術(shù)的基礎(chǔ)官網(wǎng)文檔永遠(yuǎn)是最好的第一手資料。

但是 TypeScript 的學(xué)習(xí)不能僅僅靠看官網(wǎng)文檔,你還需要?jiǎng)邮謱?shí)踐,在實(shí)踐中你才能真正掌握 TypeScript。相信很多同學(xué)學(xué)習(xí)到這里已經(jīng)迫不及待想要大展身手了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容