函式是任何應用程式的基本建構模組,無論是本機函式、從其他模組匯入的函式,還是類別上的方法。它們也是值,就像其他值一樣,TypeScript 有許多方法來描述如何呼叫函式。讓我們來瞭解如何撰寫描述函式的類型。
函式類型表達式
描述函數最簡單的方式是使用函數類型表達式。這些類型在語法上與箭頭函數類似
tsTry
functiongreeter (fn : (a : string) => void) {fn ("Hello, World");}functionprintToConsole (s : string) {console .log (s );}greeter (printToConsole );
語法 (a: string) => void
表示「一個函數,有一個參數,名稱為 a
,類型為 string
,沒有回傳值」。就像函數宣告一樣,如果未指定參數類型,則預設為 any
。
請注意,參數名稱是必需的。函數類型
(string) => void
表示「一個函數,有一個參數,名稱為string
,類型為any
」!
當然,我們可以使用類型別名來命名函數類型
tsTry
typeGreetFunction = (a : string) => void;functiongreeter (fn :GreetFunction ) {// ...}
呼叫簽章
在 JavaScript 中,函數除了可以呼叫之外,還可以有屬性。然而,函數類型表達式語法不允許宣告屬性。如果我們想要描述具有屬性的可呼叫項目,我們可以在物件類型中撰寫呼叫簽章
tsTry
typeDescribableFunction = {description : string;(someArg : number): boolean;};functiondoSomething (fn :DescribableFunction ) {console .log (fn .description + " returned " +fn (6));}functionmyFunc (someArg : number) {returnsomeArg > 3;}myFunc .description = "default description";doSomething (myFunc );
請注意,與函數類型表達式相比,語法略有不同 - 在參數清單和回傳類型之間使用 :
,而不是 =>
。
建構簽章
JavaScript 函數也可以使用 new
算子呼叫。TypeScript 將它們稱為建構函數,因為它們通常會建立一個新物件。您可以在呼叫簽章前面加上 new
關鍵字來撰寫建構簽章
tsTry
typeSomeConstructor = {new (s : string):SomeObject ;};functionfn (ctor :SomeConstructor ) {return newctor ("hello");}
某些物件,例如 JavaScript 的 Date
物件,可以呼叫或不呼叫 new
。您可以任意在同一個類型中組合呼叫和建構簽章
tsTry
interfaceCallOrConstruct {(n ?: number): string;new (s : string):Date ;}
泛型函數
通常會撰寫一個函數,其中輸入的類型與輸出的類型相關,或兩個輸入的類型在某種程度上相關。讓我們暫時考慮一個傳回陣列第一個元素的函數
tsTry
functionfirstElement (arr : any[]) {returnarr [0];}
此函數完成了它的工作,但不幸的是回傳類型為 any
。如果函數傳回陣列元素的類型會更好。
在 TypeScript 中,當我們想要描述兩個值之間的對應關係時,會使用泛型。我們透過在函式簽章中宣告類型參數來執行此動作
tsTry
functionfirstElement <Type >(arr :Type []):Type | undefined {returnarr [0];}
透過將類型參數 Type
加入這個函式並在兩個地方使用它,我們在函式的輸入(陣列)和輸出(傳回值)之間建立一個連結。現在當我們呼叫它時,會產生更具體的類型
tsTry
// s is of type 'string'consts =firstElement (["a", "b", "c"]);// n is of type 'number'constn =firstElement ([1, 2, 3]);// u is of type undefinedconstu =firstElement ([]);
推論
請注意,我們不必在這個範例中指定 Type
。類型會由 TypeScript 自動推論(選擇)。
我們也可以使用多個類型參數。例如,獨立版本的 map
會像這樣
tsTry
functionmap <Input ,Output >(arr :Input [],func : (arg :Input ) =>Output ):Output [] {returnarr .map (func );}// Parameter 'n' is of type 'string'// 'parsed' is of type 'number[]'constparsed =map (["1", "2", "3"], (n ) =>parseInt (n ));
請注意,在這個範例中,TypeScript 可以推論 Input
類型參數(從給定的 string
陣列)的類型,以及根據函式表達式的傳回值(number
)推論 Output
類型參數的類型。
約束
我們寫了一些泛型函式,它們可以在任何類型的值上運作。有時我們想要關聯兩個值,但只能在特定值子集中運作。在這種情況下,我們可以使用約束來限制類型參數可以接受的類型種類。
我們來寫一個傳回兩個值中較長者的函式。為此,我們需要一個長度為數字的 length
屬性。我們透過撰寫 extends
子句來約束類型參數為該類型
tsTry
functionlongest <Type extends {length : number }>(a :Type ,b :Type ) {if (a .length >=b .length ) {returna ;} else {returnb ;}}// longerArray is of type 'number[]'constlongerArray =longest ([1, 2], [1, 2, 3]);// longerString is of type 'alice' | 'bob'constlongerString =longest ("alice", "bob");// Error! Numbers don't have a 'length' propertyconstArgument of type 'number' is not assignable to parameter of type '{ length: number; }'.2345Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.notOK =longest (10 , 100);
在這個範例中,有幾件有趣的事情需要注意。我們允許 TypeScript 推論 longest
的傳回類型。傳回類型推論也適用於泛型函式。
由於我們將 Type
約束為 { length: number }
,因此我們可以存取 a
和 b
參數的 .length
屬性。如果沒有類型約束,我們將無法存取這些屬性,因為這些值可能是沒有長度屬性的其他類型。
longerArray
和 longerString
的類型是根據引數推論出來的。請記住,泛型就是將兩個或多個具有相同類型的值關聯起來!
最後,正如我們所希望的,對 longest(10, 100)
的呼叫會被拒絕,因為 number
類型沒有 .length
屬性。
使用受約束值
以下是使用泛型約束時常見的錯誤
tsTry
functionminimumLength <Type extends {length : number }>(obj :Type ,minimum : number):Type {if (obj .length >=minimum ) {returnobj ;} else {return {Type '{ length: number; }' is not assignable to type 'Type'. '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.2322Type '{ length: number; }' is not assignable to type 'Type'. '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.length :minimum };}}
這個函式看起來沒問題 - Type
約束為 { length: number }
,而且函式會傳回 Type
或符合該約束的值。問題在於函式承諾會傳回與傳入的物件相同類型的物件,而不是僅傳回符合約束的某些物件。如果這段程式碼是合法的,你可以撰寫絕對無法運作的程式碼
tsTry
// 'arr' gets value { length: 6 }constarr =minimumLength ([1, 2, 3], 6);// and crashes here because arrays have// a 'slice' method, but not the returned object!console .log (arr .slice (0));
指定類型引數
TypeScript 通常可以推論泛型呼叫中的預期類型引數,但並非總是如此。例如,假設你撰寫了一個函式來合併兩個陣列
tsTry
functioncombine <Type >(arr1 :Type [],arr2 :Type []):Type [] {returnarr1 .concat (arr2 );}
通常,使用不匹配的陣列呼叫此函式會產生錯誤
tsTry
constType 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.arr =combine ([1, 2, 3], ["hello" ]);
不過,如果您打算這樣做,您可以手動指定 Type
tsTry
constarr =combine <string | number>([1, 2, 3], ["hello"]);
撰寫良好泛型函式的準則
撰寫泛型函式很有趣,而且很容易沉迷於類型參數。類型參數太多或在不需要的地方使用約束可能會降低推論的成功率,讓函式呼叫者感到沮喪。
向下推類型參數
以下是撰寫看似類似的函式的兩種方式
tsTry
functionfirstElement1 <Type >(arr :Type []) {returnarr [0];}functionfirstElement2 <Type extends any[]>(arr :Type ) {returnarr [0];}// a: number (good)consta =firstElement1 ([1, 2, 3]);// b: any (bad)constb =firstElement2 ([1, 2, 3]);
乍看之下,這些函式可能看起來相同,但 firstElement1
是撰寫此函式更好的方式。其推論的回傳類型為 Type
,但 firstElement2
的推論回傳類型為 any
,因為 TypeScript 必須使用約束類型來解析 arr[0]
表達式,而不是在呼叫期間「等待」解析元素。
規則:如果可能,請使用類型參數本身,而不是約束它
使用較少的類型參數
以下是另一對類似的函式
tsTry
functionfilter1 <Type >(arr :Type [],func : (arg :Type ) => boolean):Type [] {returnarr .filter (func );}functionfilter2 <Type ,Func extends (arg :Type ) => boolean>(arr :Type [],func :Func ):Type [] {returnarr .filter (func );}
我們建立了一個類型參數 Func
,它不關聯兩個值。這總是一個警訊,因為這表示想要指定類型參數的呼叫者必須無緣無故手動指定額外的類型參數。Func
除了讓函式更難閱讀和理解之外,什麼事都不做!
規則:盡可能使用較少的類型參數
類型參數應出現兩次
有時我們會忘記一個函式可能不需要是泛型的
tsTry
functiongreet <Str extends string>(s :Str ) {console .log ("Hello, " +s );}greet ("world");
我們可以很輕易地寫出一個更簡單的版本
tsTry
functiongreet (s : string) {console .log ("Hello, " +s );}
請記住,類型參數用於關聯多個值的類型。如果類型參數只在函式簽章中使用一次,它就不會關聯任何東西。這包括推斷的回傳類型;例如,如果 Str
是 greet
推斷的回傳類型的一部分,它將關聯參數和回傳類型,因此會被使用兩次,儘管在寫好的程式碼中只出現一次。
規則:如果類型參數只出現在一個位置,請仔細考慮你是否真的需要它
選用參數
JavaScript 中的函式通常會採用變數個數的參數。例如,number
的 toFixed
方法會採用一個選用的位數計數
tsTry
functionf (n : number) {console .log (n .toFixed ()); // 0 argumentsconsole .log (n .toFixed (3)); // 1 argument}
我們可以在 TypeScript 中透過使用 ?
將參數標記為選用來建模這個方法
tsTry
functionf (x ?: number) {// ...}f (); // OKf (10); // OK
儘管參數指定為類型 number
,但 x
參數實際上會有類型 number | undefined
,因為 JavaScript 中未指定的參數會取得值 undefined
。
你也可以提供參數預設值
tsTry
functionf (x = 10) {// ...}
現在在 f
的主體中,x
會有類型 number
,因為任何 undefined
參數都會被替換為 10
。請注意,當參數是選用的,呼叫者總是能傳遞 undefined
,因為這只是模擬一個「遺失」的參數
tsTry
// All OKf ();f (10);f (undefined );
選用參數在呼叫回函
一旦你了解選用參數和函式類型表達式後,在撰寫呼叫呼叫回函的函式時,很容易發生下列錯誤
tsTry
functionmyForEach (arr : any[],callback : (arg : any,index ?: number) => void) {for (leti = 0;i <arr .length ;i ++) {callback (arr [i ],i );}}
人們通常在撰寫 index?
作為選用參數時,希望這兩個呼叫都是合法的
tsTry
myForEach ([1, 2, 3], (a ) =>console .log (a ));myForEach ([1, 2, 3], (a ,i ) =>console .log (a ,i ));
這實際上表示callback
可能會呼叫一個參數。換句話說,函式定義表示實作可能如下所示
tsTry
functionmyForEach (arr : any[],callback : (arg : any,index ?: number) => void) {for (leti = 0;i <arr .length ;i ++) {// I don't feel like providing the index todaycallback (arr [i ]);}}
反過來,TypeScript 將強制執行此含義,並發出實際上不可能發生的錯誤
tsTry
myForEach ([1, 2, 3], (a ,i ) => {'i' is possibly 'undefined'.18048'i' is possibly 'undefined'.console .log (. i toFixed ());});
在 JavaScript 中,如果你呼叫函式時參數多於參數數量,額外的參數將會被忽略。TypeScript 的行為相同。參數較少的函式(類型相同)永遠可以取代參數較多的函式。
規則:撰寫呼叫回函的函式類型時,絕不撰寫選用參數,除非你打算呼叫函式而不傳遞該參數
函式超載
有些 JavaScript 函式可以在各種參數計數和類型中呼叫。例如,你可以撰寫一個函式來產生 Date
,它可以接受時間戳記(一個參數)或月份/日期/年份規格(三個參數)。
在 TypeScript 中,我們可以透過撰寫超載簽章來指定一個函式,它可以用不同的方式呼叫。為此,請撰寫一些函式簽章(通常是兩個或更多),然後是函式主體
tsTry
functionmakeDate (timestamp : number):Date ;functionmakeDate (m : number,d : number,y : number):Date ;functionmakeDate (mOrTimestamp : number,d ?: number,y ?: number):Date {if (d !==undefined &&y !==undefined ) {return newDate (y ,mOrTimestamp ,d );} else {return newDate (mOrTimestamp );}}constd1 =makeDate (12345678);constd2 =makeDate (5, 5, 5);constNo overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.2575No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.d3 =makeDate (1, 3);
在此範例中,我們撰寫了兩個超載:一個接受一個參數,另一個接受三個參數。這前兩個簽章稱為超載簽章。
然後,我們撰寫了一個具有相容簽章的函式實作。函式有一個實作簽章,但無法直接呼叫此簽章。即使我們在必需參數後撰寫了一個具有兩個選用參數的函式,但它無法呼叫兩個參數!
重載簽章和實作簽章
這是常見的混淆來源。人們經常會寫出像這樣的程式碼,卻不明白為什麼會出錯
tsTry
functionfn (x : string): void;functionfn () {// ...}// Expected to be able to call with zero argumentsExpected 1 arguments, but got 0.2554Expected 1 arguments, but got 0.fn ();
同樣地,用來撰寫函式主體的簽章無法從外部「看到」。
實作 的簽章從外部不可見。在撰寫重載函式時,函式實作上方應該永遠有 兩個 或以上的簽章。
實作簽章也必須與重載簽章相容。例如,以下函式有錯誤,因為實作簽章與重載沒有正確對應
tsTry
functionfn (x : boolean): void;// Argument type isn't rightfunctionThis overload signature is not compatible with its implementation signature.2394This overload signature is not compatible with its implementation signature.( fn x : string): void;functionfn (x : boolean) {}
tsTry
functionfn (x : string): string;// Return type isn't rightfunctionThis overload signature is not compatible with its implementation signature.2394This overload signature is not compatible with its implementation signature.( fn x : number): boolean;functionfn (x : string | number) {return "oops";}
撰寫良好的重載
與泛型一樣,在使用函式重載時,您應該遵循一些準則。遵循這些原則將使您的函式更容易呼叫、更容易理解,並且更容易實作。
讓我們考慮一個傳回字串或陣列長度的函式
tsTry
functionlen (s : string): number;functionlen (arr : any[]): number;functionlen (x : any) {returnx .length ;}
這個函式很好;我們可以用字串或陣列呼叫它。但是,我們無法使用可能是字串或陣列的值呼叫它,因為 TypeScript 只可以將函式呼叫解析為單一重載
tsTry
len (""); // OKlen ([0]); // OKNo overload matches this call. Overload 1 of 2, '(s: string): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'. Type 'number[]' is not assignable to type 'string'. Overload 2 of 2, '(arr: any[]): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'. Type 'string' is not assignable to type 'any[]'.2769No overload matches this call. Overload 1 of 2, '(s: string): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'. Type 'number[]' is not assignable to type 'string'. Overload 2 of 2, '(arr: any[]): number', gave the following error. Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'. Type 'string' is not assignable to type 'any[]'.len (Math .random () > 0.5 ? "hello" : [0]);
由於兩個重載都有相同的參數數量和相同的回傳類型,我們可以改寫成非重載版本的函式
tsTry
functionlen (x : any[] | string) {returnx .length ;}
這樣好多了!呼叫者可以使用任一種值呼叫此函式,而且額外的好處是,我們不必找出正確的實作簽章。
如果可以,請務必優先使用具有聯集類型的參數,而不是重載
在函式中宣告 this
TypeScript 會透過程式碼流程分析來推論函式中的 this
應該是什麼,例如以下範例
tsTry
constuser = {id : 123,admin : false,becomeAdmin : function () {this.admin = true;},};
TypeScript 了解函式 user.becomeAdmin
有對應的 this
,也就是外部物件 user
。this
對於許多情況來說已經足夠了,但有許多情況需要更進一步控制 this
所代表的物件。JavaScript 規範指出您不能有一個稱為 this
的參數,因此 TypeScript 使用該語法空間讓您在函式主體中宣告 this
的類型。
tsTry
interfaceDB {filterUsers (filter : (this :User ) => boolean):User [];}constdb =getDB ();constadmins =db .filterUsers (function (this :User ) {return this.admin ;});
這種模式常見於回呼式 API,其中另一個物件通常會控制您的函式何時被呼叫。請注意,您需要使用 function
而不是箭頭函式才能獲得此行為
tsTry
interfaceDB {filterUsers (filter : (this :User ) => boolean):User [];}constdb =getDB ();constThe containing arrow function captures the global value of 'this'.admins =db .filterUsers (() =>this .); admin
Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.7041
7017The containing arrow function captures the global value of 'this'.
Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
其他需要了解的類型
在使用函式類型時,有一些額外的類型您會想要認識,它們經常出現。與所有類型一樣,您可以在任何地方使用它們,但這些類型在函式的脈絡中特別相關。
void
void
代表不回傳值的函式的回傳值。當函式沒有任何 return
陳述式,或從這些回傳陳述式回傳任何明確的值時,它就是推論的型別。
tsTry
// The inferred return type is voidfunctionnoop () {return;}
在 JavaScript 中,不回傳任何值的函式會隱含回傳 undefined
值。然而,在 TypeScript 中,void
和 undefined
並不相同。本章節的最後面有進一步的詳細資訊。
void
與undefined
不同。
object
特殊型別 object
指涉任何非原生的值(string
、number
、bigint
、boolean
、symbol
、null
或 undefined
)。這與空物件型別 { }
不同,也與全域型別 Object
不同。您很可能永遠不會使用 Object
。
object
不是Object
。始終使用object
!
請注意,在 JavaScript 中,函式值是物件:它們有屬性,在它們的原型鏈中有 Object.prototype
,是 instanceof Object
,您可以在它們上呼叫 Object.keys
,等等。因此,函式型別在 TypeScript 中被視為 object
。
unknown
unknown
型別代表任何值。這類似於 any
型別,但更安全,因為無法對 unknown
值執行任何操作。
tsTry
functionf1 (a : any) {a .b (); // OK}functionf2 (a : unknown) {'a' is of type 'unknown'.18046'a' is of type 'unknown'.. a b ();}
在描述函式型別時,這很有用,因為您可以在函式主體中沒有 any
值的情況下描述接受任何值的函式。
相反地,您可以描述傳回未知型別值的函式
tsTry
functionsafeParse (s : string): unknown {returnJSON .parse (s );}// Need to be careful with 'obj'!constobj =safeParse (someRandomString );
never
某些函數永遠不會傳回值
tsTry
functionfail (msg : string): never {throw newError (msg );}
never
類型代表永遠不會觀察到的值。在傳回類型中,這表示函數會擲回例外或終止程式執行。
當 TypeScript 判斷聯集內沒有任何內容時,也會出現 never
。
tsTry
functionfn (x : string | number) {if (typeofx === "string") {// do something} else if (typeofx === "number") {// do something else} else {x ; // has type 'never'!}}
Function
全域類型 Function
描述 JavaScript 中所有函數值上存在的屬性,例如 bind
、call
、apply
等。它還有一個特殊屬性,即類型為 Function
的值總是可呼叫的;這些呼叫會傳回 any
tsTry
functiondoSomething (f :Function ) {returnf (1, 2, 3);}
這是一個未輸入型別的函數呼叫,由於不安全的 any
傳回類型,通常最好避免使用。
如果您需要接受任意函數,但無意呼叫它,則類型 () => void
通常較安全。
Rest 參數和引數
Rest 參數
除了使用選用參數或重載來建立可接受各種固定引數數量的函數外,我們還可以定義使用rest 參數來取得無界數量的引數的函數。
休息參數出現在所有其他參數之後,並使用 ...
語法
tsTry
functionmultiply (n : number, ...m : number[]) {returnm .map ((x ) =>n *x );}// 'a' gets value [10, 20, 30, 40]consta =multiply (10, 1, 2, 3, 4);
在 TypeScript 中,這些參數的類型註解隱含為 any[]
,而不是 any
,並且給定的任何類型註解都必須為 Array<T>
或 T[]
形式,或元組類型(我們稍後會學習)。
休息參數
相反地,我們可以使用擴散語法從可迭代物件(例如陣列)提供變數數量的參數。例如,陣列的 push
方法會接收任意數量的參數
tsTry
constarr1 = [1, 2, 3];constarr2 = [4, 5, 6];arr1 .push (...arr2 );
請注意,一般來說,TypeScript 不會假設陣列是不可變的。這可能會導致一些令人驚訝的行為
tsTry
// Inferred type is number[] -- "an array with zero or more numbers",// not specifically two numbersconstargs = [8, 5];constA spread argument must either have a tuple type or be passed to a rest parameter.2556A spread argument must either have a tuple type or be passed to a rest parameter.angle =Math .atan2 (...args );
這種情況的最佳修復方式取決於您的程式碼,但一般來說,const
環境是最直接的解決方案
tsTry
// Inferred as 2-length tupleconstargs = [8, 5] asconst ;// OKconstangle =Math .atan2 (...args );
使用休息參數可能需要在目標為舊執行時期時開啟 downlevelIteration
。
參數解構
背景讀物
解構指定
您可以使用參數解構來方便地將提供為參數的物件解壓縮到函式主體中的一個或多個局部變數中。在 JavaScript 中,它看起來像這樣
js
function sum({ a, b, c }) {console.log(a + b + c);}sum({ a: 10, b: 3, c: 9 });
物件的類型註解出現在解構語法之後
tsTry
functionsum ({a ,b ,c }: {a : number;b : number;c : number }) {console .log (a +b +c );}
這看起來可能有點冗長,但您也可以在此處使用命名類型
tsTry
// Same as prior exampletypeABC = {a : number;b : number;c : number };functionsum ({a ,b ,c }:ABC ) {console .log (a +b +c );}