函式是任何應用程式的基本構建塊,無論是本地函式、從其他模組匯入的函式,還是類中的方法。它們也是值,並且和其他值一樣,TypeScript 有多種方式來描述函式的呼叫方式。讓我們來學習如何編寫描述函式的型別。
函式型別表示式
描述函式最簡單的方法是使用函式型別表示式。這些型別的語法與箭頭函式類似
tsTryfunctiongreeter (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的引數的函式”!
當然,我們可以使用類型別名來為函式型別命名
tsTrytypeGreetFunction = (a : string) => void;functiongreeter (fn :GreetFunction ) {// ...}
呼叫簽名
在 JavaScript 中,函式除了可以被呼叫之外,還可以擁有屬性。然而,函式型別表示式語法不允許宣告屬性。如果我們想描述一個既可呼叫又帶有屬性的物件,可以在物件型別中編寫呼叫簽名
tsTrytypeDescribableFunction = {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 關鍵字來編寫構造簽名
tsTrytypeSomeConstructor = {new (s : string):SomeObject ;};functionfn (ctor :SomeConstructor ) {return newctor ("hello");}
有些物件(如 JavaScript 的 Date 物件)既可以帶 new 呼叫,也可以不帶 new 呼叫。你可以在同一個型別中隨意組合呼叫簽名和構造簽名
tsTryinterfaceCallOrConstruct {(n ?: number): string;new (s : string):Date ;}functionfn (ctor :CallOrConstruct ) {// Passing an argument of type `number` to `ctor` matches it against// the first definition in the `CallOrConstruct` interface.console .log (ctor (10));// Similarly, passing an argument of type `string` to `ctor` matches it// against the second definition in the `CallOrConstruct` interface.console .log (newctor ("10"));}fn (Date );
泛型函式
編寫函式時,輸入型別與輸出型別相關,或者兩個輸入的型別以某種方式相關是很常見的。讓我們考慮一個返回陣列第一個元素的函式
tsTryfunctionfirstElement (arr : any[]) {returnarr [0];}
這個函式完成了任務,但遺憾的是返回型別為 any。如果函式能返回陣列元素的型別就更好了。
在 TypeScript 中,當我們想要描述兩個值之間的對應關係時,會使用泛型。我們透過在函式簽名中宣告型別引數來實現這一點
tsTryfunctionfirstElement <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 看起來像這樣
tsTryfunctionmap <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 子句將型別引數約束為該型別
tsTryfunctionlongest <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 屬性。如果沒有型別約束,我們將無法訪問這些屬性,因為這些值可能是其他沒有 length 屬性的型別。
longerArray 和 longerString 的型別是根據引數推斷出來的。記住,泛型的核心就是關聯兩個或多個相同型別的變數!
最後,正如我們所願,呼叫 longest(10, 100) 會被拒絕,因為 number 型別沒有 .length 屬性。
使用受約束的值
在使用泛型約束時,這是一個常見的錯誤
tsTryfunctionminimumLength <Type extends {length : number }>(obj :Type ,minimum : number):Type {if (obj .length >=minimum ) {returnobj ;} else {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; }'.return {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 通常可以推斷泛型呼叫中的預期型別引數,但並不總是如此。例如,假設你編寫了一個組合兩個陣列的函式
tsTryfunctioncombine <Type >(arr1 :Type [],arr2 :Type []):Type [] {returnarr1 .concat (arr2 );}
通常情況下,用不匹配的陣列呼叫此函式會報錯
tsTryconstType 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.arr =combine ([1, 2, 3], ["hello" ]);
然而,如果你確實打算這樣做,可以手動指定 Type
tsTryconstarr =combine <string | number>([1, 2, 3], ["hello"]);
編寫優秀泛型函式的準則
編寫泛型函式很有趣,但也容易過度使用型別引數。引數過多或在不需要的地方使用約束會降低推斷的成功率,讓呼叫你函式的開發者感到困惑。
下推型別引數
這是兩種看起來相似的編寫函式的方法
tsTryfunctionfirstElement1 <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] 表示式,而不是“等待”在呼叫期間解析元素。
規則:儘可能直接使用型別引數本身,而不是對其進行約束
少用型別引數
這裡有另一對類似的函式
tsTryfunctionfilter1 <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 除了讓函式更難閱讀和推理外沒有任何作用!
規則:始終使用盡可能少的型別引數
型別引數應出現兩次
有時我們忘了函式可能不需要是泛型的
tsTryfunctiongreet <Str extends string>(s :Str ) {console .log ("Hello, " +s );}greet ("world");
我們可以輕鬆寫出一個更簡單的版本
tsTryfunctiongreet (s : string) {console .log ("Hello, " +s );}
記住,型別引數用於關聯多個值的型別。如果一個型別引數在函式簽名中只使用了一次,它就沒有起到任何關聯作用。這包括推斷出的返回型別;例如,如果 Str 是 greet 推斷返回型別的一部分,它將關聯引數和返回型別,即使在編寫的程式碼中它只出現了一次,它實際上也被使用了兩次。
規則:如果型別引數只在一個位置出現,請務必重新考慮是否真的需要它
可選引數
JavaScript 中的函式通常接受可變數量的引數。例如,number 的 toFixed 方法接受一個可選的數字精度
tsTryfunctionf (n : number) {console .log (n .toFixed ()); // 0 argumentsconsole .log (n .toFixed (3)); // 1 argument}
我們可以透過用 ? 將引數標記為可選來在 TypeScript 中進行建模
tsTryfunctionf (x ?: number) {// ...}f (); // OKf (10); // OK
雖然引數被指定為 number 型別,但 x 引數實際上具有 number | undefined 型別,因為 JavaScript 中未指定的引數會得到 undefined 值。
你也可以提供一個引數預設值
tsTryfunctionf (x = 10) {// ...}
現在在 f 的主體中,x 的型別為 number,因為任何 undefined 引數都將被替換為 10。注意,當引數是可選的時,呼叫者可以隨時傳入 undefined,因為這僅僅是模擬了一個“缺失”的引數
tsTry// All OKf ();f (10);f (undefined );
回撥中的可選引數
一旦你學習了可選引數和函式型別表示式,在編寫呼叫回撥的函式時,很容易犯以下錯誤
tsTryfunctionmyForEach (arr : any[],callback : (arg : any,index ?: number) => void) {for (leti = 0;i <arr .length ;i ++) {callback (arr [i ],i );}}
人們在寫 index? 作為可選引數時,通常的意圖是希望這兩種呼叫都是合法的
tsTrymyForEach ([1, 2, 3], (a ) =>console .log (a ));myForEach ([1, 2, 3], (a ,i ) =>console .log (a ,i ));
這實際上意味著 callback 可能會以一個引數被呼叫。換句話說,函式定義聲明瞭實現可能看起來像這樣
tsTryfunctionmyForEach (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 會強制執行這一含義,併發出原本不應該出現的錯誤
tsTrymyForEach ([1, 2, 3], (a ,i ) => {'i' is possibly 'undefined'.18048'i' is possibly 'undefined'.console .log (. i toFixed ());});
在 JavaScript 中,如果你呼叫的函式引數多於引數定義數量,多餘的引數會被簡單地忽略。TypeScript 的表現方式相同。引數較少(且型別相容)的函式總是可以替代引數較多的函式。
規則:在為回撥函式編寫函式型別時,永遠不要寫可選引數,除非你的意圖是在不傳入該引數的情況下呼叫該函式
函式過載
某些 JavaScript 函式可以以不同的引數數量和型別進行呼叫。例如,你可能編寫了一個生成 Date 的函式,它接受時間戳(一個引數)或月/日/年規範(三個引數)。
在 TypeScript 中,我們可以透過編寫過載簽名來指定一個可以以不同方式呼叫的函式。為此,請編寫若干函式簽名(通常是兩個或更多),後跟函式的實現主體
tsTryfunctionmakeDate (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 =(1, 3); makeDate
在這個示例中,我們編寫了兩個過載:一個接受一個引數,另一個接受三個引數。前兩個簽名被稱為過載簽名。
然後,我們編寫了一個具有相容簽名的函式實現。函式擁有一個實現簽名,但此簽名不能直接呼叫。即使我們在必選引數後編寫了兩個可選引數的函式,它也不能被兩個引數呼叫!
過載簽名與實現簽名
這是一個常見的困惑來源。人們經常編寫類似下面的程式碼,卻不理解為什麼會報錯
tsTryfunctionfn (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
同樣,用於編寫函式體的簽名在外部是“不可見”的。
實現的簽名在外部是不可見的。編寫過載函式時,應始終在函式實現之上編寫兩個或更多簽名。
實現簽名必須與過載簽名相容。例如,以下函式會報錯,因為實現簽名與過載簽名不匹配
tsTryfunctionfn (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) {}
tsTryfunctionfn (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";}
編寫優秀的過載
像泛型一樣,使用函式過載時也有一些準則。遵循這些原則將使你的函式更易於呼叫、更易於理解、更易於實現。
讓我們考慮一個返回字串或陣列長度的函式
tsTryfunctionlen (s : string): number;functionlen (arr : any[]): number;functionlen (x : any) {returnx .length ;}
這個函式沒問題;我們可以用字串或陣列呼叫它。但是,我們不能用可能是字串或陣列的值呼叫它,因為 TypeScript 只能將函式呼叫解析為單個過載
tsTrylen (""); // 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]);
由於兩個過載具有相同的引數數量和相同的返回型別,我們可以改為編寫一個非過載版本的函式
tsTryfunctionlen (x : any[] | string) {returnx .length ;}
這好多了!呼叫者可以用任意一種值呼叫它,而且作為一個額外的獎勵,我們不必計算正確的實現簽名。
儘可能優先使用聯合型別的引數,而不是過載
在函式中宣告 this
TypeScript 將透過程式碼流分析來推斷函式中的 this 應該是什麼,例如在以下情況中
tsTryconstuser = {id : 123,admin : false,becomeAdmin : function () {this.admin = true;},};
TypeScript 明白函式 user.becomeAdmin 有一個相應的 this,即外部物件 user。這對很多情況來說已經足夠了,但在很多情況下,你需要更好地控制 this 代表哪個物件。JavaScript 規範規定你不能有一個名為 this 的引數,因此 TypeScript 使用該語法空間來讓你在函式體中宣告 this 的型別。
tsTryinterfaceDB {filterUsers (filter : (this :User ) => boolean):User [];}constdb =getDB ();constadmins =db .filterUsers (function (this :User ) {return this.admin ;});
這種模式在回撥風格的 API 中很常見,通常由另一個物件控制你的函式何時被呼叫。注意,你需要使用 function 而不是箭頭函式來獲得此行為
tsTryinterfaceDB {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 語句或沒有從這些 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 值進行任何操作都是不合法的
tsTryfunctionf1 (a : any) {a .b (); // OK}functionf2 (a : unknown) {'a' is of type 'unknown'.18046'a' is of type 'unknown'.. a b ();}
在描述函式型別時,這很有用,因為你可以描述接受任何值的函式,而無需在函式體內使用 any。
相反,你可以描述一個返回未知型別值的函式
tsTryfunctionsafeParse (s : string): unknown {returnJSON .parse (s );}// Need to be careful with 'obj'!constobj =safeParse (someRandomString );
never
有些函式從不返回值
tsTryfunctionfail (msg : string): never {throw newError (msg );}
never 型別代表永遠不會觀察到的值。在返回型別中,這意味著函式丟擲異常或終止程式執行。
當 TypeScript 確定聯合型別中沒有任何剩餘項時,也會出現 never。
tsTryfunctionfn (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
tsTryfunctiondoSomething (f :Function ) {returnf (1, 2, 3);}
這是一種無型別函式呼叫,由於不安全的 any 返回型別,通常最好避免使用。
如果你需要接受一個任意函式但不打算呼叫它,() => void 型別通常更安全。
剩餘引數和引數
剩餘引數
除了使用可選引數或過載來建立可接受多種固定數量引數的函式外,我們還可以使用剩餘引數定義接受無限數量引數的函式。
剩餘引數出現在所有其他引數之後,並使用 ... 語法
tsTryfunctionmultiply (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 方法接受任意數量的引數
tsTryconstarr1 = [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 中,它看起來是這樣的
jsfunction sum({ a, b, c }) {console.log(a + b + c);}sum({ a: 10, b: 3, c: 9 });
物件的型別註釋位於解構語法之後
tsTryfunctionsum ({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 );}
函式的可賦值性
返回型別 void
函式的 void 返回型別可能會產生一些不尋常但符合預期的行為。
具有 void 返回型別的上下文型別化不會強制函式不返回任何東西。換句話說,當一個具有 void 返回型別的上下文函式型別(type voidFunc = () => void)被實現時,它可以返回任何其他值,但該值將被忽略。
因此,以下對 () => void 型別的實現是有效的
tsTrytypevoidFunc = () => void;constf1 :voidFunc = () => {return true;};constf2 :voidFunc = () => true;constf3 :voidFunc = function () {return true;};
當這些函式之一的返回值被分配給另一個變數時,它將保持 void 型別
tsTryconstv1 =f1 ();constv2 =f2 ();constv3 =f3 ();
存在這種行為是為了讓以下程式碼有效,即使 Array.prototype.push 返回一個數字,而 Array.prototype.forEach 方法期望一個返回型別為 void 的函式。
tsTryconstsrc = [1, 2, 3];constdst = [0];src .forEach ((el ) =>dst .push (el ));
還有另一種需要注意的特殊情況,當字面函式定義具有 void 返回型別時,該函式必須不返回任何東西。
tsTryfunctionf2 (): void {// @ts-expect-errorreturn true;}constf3 = function (): void {// @ts-expect-errorreturn true;};
關於 void 的更多資訊,請參考這些其他文件條目