TypeScript'te Fonksiyonların Tipleri: Parametreler, Dönüş Tipi ve Overload'lar
Parametre ve dönüş tiplerini; optional, default ve rest parametreleri, function type expression ve call signature'ı, void dönüşün inceliğini, overload'ları ve this parametresini örneklerle görüyoruz.
Sürüm notu: Bu yazıdaki kodlar TypeScript 7.0 ve Node 26 içindir.
Önce Tahmin Et
Bu yazıda fonksiyonların tiplerine yakından bakacağız. İlk soru void hakkında. Aşağıdaki kodda hangi satırlar hata verir?
type Logger = (message: string) => void;
const log: Logger = (message) => message.length;
const numbers = [1, 2, 3];
const copy: number[] = [];
numbers.forEach((n) => copy.push(n));
function reset(): void {
return 0;
}
void'in "bu fonksiyon değer döndürmez" anlamına geldiğini biliyoruz. log bir sayı döndürüyor, copy.push da öyle; reset ise 0 döndürüyor. Üç yerden kaçı hata? Cevap, void dönüşün inceliğini konuştuğumuz bölümde.
Parametreler ve Dönüş Tipi
Inference'ı konuştuğumuz yazıda temel kuralı koymuştuk: parametrelere annotation yazarsın, dönüş tipini çoğu zaman TypeScript çıkarır. Dönüş tipini açıkça yazmak ise bir sözleşmedir; fonksiyonun gövdesi değiştiğinde yanlışlıkla başka bir tip döndürmeye başlarsan hatayı fonksiyonun içinde görürsün, onu kullanan onlarca yerde değil.
function calculateTotal(price: number, quantity: number): number {
return price * quantity;
}
// Hata: Expected 2 arguments, but got 1.
calculateTotal(10);
// Hata: Expected 2 arguments, but got 3.
calculateTotal(10, 2, 5);
JavaScript'te bir fonksiyonu eksik ya da fazla argümanla çağırmak serbesttir; eksik parametreler undefined olur, fazlası sessizce yok sayılır. TypeScript ise argüman sayısını da denetler. Parametre listesi, fonksiyonun tam olarak neyi beklediğini anlatan bir sözleşme hâline gelir.
Optional, Default ve Rest Parametreler
Her parametre zorunlu olmak zorunda değil. TypeScript, JavaScript'te zaten yazdığın kalıpları tip seviyesinde de tarif etmeni sağlar:
function greet(name: string, greeting?: string) {
// greeting: string | undefined
return `${greeting ?? "Hello"}, ${name}!`;
}
function repeat(text: string, times = 2) {
// times: number
return text.repeat(times);
}
function sum(...numbers: number[]) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(greet("Ada")); // Hello, Ada!
console.log(greet("Ada", "Welcome")); // Welcome, Ada!
console.log(repeat("ha")); // haha
console.log(sum(1, 2, 3)); // 6
Parametre adından sonra gelen ?, onu optional yapar. Fonksiyonun içinde tipi string | undefined olur, yani kullanmadan önce undefined ihtimalini ele alman gerekir. Optional parametreler zorunlu olanlardan sonra gelmelidir; tersini yazarsan TypeScript A required parameter cannot follow an optional parameter. der.
Default değerli parametrede annotation'a çoğu zaman gerek yoktur: times = 2 yazdığında TypeScript tipi number olarak çıkarır. Fonksiyonu çağıran için bu parametre optional'dır, ama fonksiyonun içinde asla undefined olmaz.
Rest parametrenin tipi her zaman bir array'dir: ...numbers: number[], "istediğin kadar number" demektir. Daha önce gördüğümüz tuple'larla da birleşebilir, ama günlük kodda düz array yeterlidir.
Fonksiyonun Kendi Tipi
Şimdiye kadar fonksiyonları tanımlarken tipledik. Peki bir fonksiyonu parametre olarak alacaksan ya da bir değişkende tutacaksan, o fonksiyonun tipini nasıl yazarsın? Function type expression ile:
type Formatter = (value: number) => string;
const toCurrency: Formatter = (value) => `$${value.toFixed(2)}`;
const toPercent: Formatter = (value) => `${Math.round(value * 100)}%`;
function formatAll(values: number[], format: Formatter) {
return values.map(format);
}
console.log(formatAll([0.25, 0.5], toPercent)); // [ '25%', '50%' ]
console.log(toCurrency(3)); // $3.00
(value: number) => string, "bir number alan ve string döndüren fonksiyon" demektir. Arrow function'a benziyor ama bir değer değil, bir tip; ok işaretinin sağında gövde değil, dönüş tipi var. Tipteki parametre adı yalnızca okunabilirlik içindir; atadığın fonksiyonun parametresine istediğin ismi verebilirsin.
Dikkat edersen toCurrency ve toPercent'te value'ya annotation yazmadık. TypeScript fonksiyonun hangi tipe atandığını bildiği için parametre tipini oradan alır. Buna contextual typing denir ve map, filter, forEach gibi method'lara verdiğin callback'lerde neden hiç tip yazmadığının da cevabıdır.
Bir incelik daha: daha az parametre alan bir fonksiyon, daha fazla parametre bekleyen bir fonksiyon tipine atanabilir. const ignore: Formatter = () => "n/a"; geçerlidir. JavaScript'te fazla argümanlar zaten yok sayıldığı için bu güvenlidir; forEach'e üç parametre yerine tek parametreli bir callback verebilmen de bu kural sayesindedir.
Call Signature: Property'si Olan Fonksiyonlar
JavaScript'te fonksiyonlar da birer object'tir ve property taşıyabilir. Function type expression bunu ifade edemez; bunun için object type içinde bir call signature yazarsın:
type Validator = {
(value: string): boolean;
description: string;
};
function isEmail(value: string) {
return value.includes("@");
}
isEmail.description = "Contains an @ sign";
const check: Validator = isEmail;
console.log(check.description, check("ada@example.com")); // Contains an @ sign true
(value: string): boolean satırı, bu object'in bir string ile çağrılabildiğini ve boolean döndürdüğünü söyler. Function type expression'dan farkı, okun yerine iki nokta kullanılmasıdır. Aynı tipi type alias yerine interface ile de yazabilirsin; gövde birebir aynı kalır. TypeScript, bir fonksiyon tanımından hemen sonra ona atadığın property'leri de tanır; description'ı eklemeseydin check satırı Property 'description' is missing diye başlayan bir hata verirdi.
void Dönüşün İnceliği
Baştaki sorunun cevabı: yalnızca reset içindeki return 0 hata verir.
error TS2322: Type 'number' is not assignable to type 'void'.
Bu ilk bakışta tutarsız görünür, ama arkasında mantıklı bir ayrım var. Bir fonksiyon tanımına doğrudan : void yazdığında "bu fonksiyon değer döndürmeyecek" diye söz vermiş olursun ve TypeScript bu sözü denetler. Bir fonksiyon tipindeki void ise farklı bir şey söyler: "dönüş değeriyle ilgilenmiyorum." Logger tipine uyan bir fonksiyon değer döndürebilir; sadece o değer kullanılmayacaktır.
Bu kural olmasaydı numbers.forEach((n) => copy.push(n)) gibi masum bir satır derlenmezdi. forEach'in beklediği callback'in dönüş tipi void, push ise yeni uzunluğu, yani bir number döndürür. TypeScript burada esnek davranır, çünkü forEach o değere zaten bakmaz.
Bunun bir sonucu da var: const result = log("hi"); yazarsan result'ın tipi void olur, gerçekte bir sayı taşısa bile. Tip, değerin kullanılmaması gerektiğini söylüyor; ona güvenip bir şey yapamazsın.
Overload'lar
Bazen bir fonksiyonun dönüş tipi, aldığı argümana bağlıdır. Tek bir string verirsen tek bir sayı, string array'i verirsen sayı array'i dönsün istiyorsun. Union ile yazarsan dönüş tipi her zaman number | number[] olur ve çağıran taraf her seferinde daraltmak zorunda kalır. Overload'lar bu ilişkiyi tipe işlemenin yoludur:
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]) {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
const single = parse("42"); // single: number
const many = parse(["1", "2"]); // many: number[]
İlk iki satır overload signature'lardır: gövdeleri yoktur ve fonksiyonun dışarıdan nasıl çağrılabileceğini listeler. Üçüncüsü implementation signature'dır; bütün overload'ları karşılayacak kadar geniş olmalıdır ve gerçek kodu taşır. Çağrı yapıldığında TypeScript overload'ları yukarıdan aşağıya dener ve ilk uyanı seçer. Implementation signature ise dışarıdan görünmez; fonksiyonu yalnızca overload'larda yazdığın şekillerde çağırabilirsin.
Overload signature'lar sadece tip bilgisidir ve JavaScript'e derlenirken silinir. Yani daha önce konuştuğumuz node file.ts ile doğrudan çalışırlar.
Yine de ilk tercih overload olmamalı. Dönüş tipi argümana bağlı değilse, union parametreli tek bir imza hem daha kısa hem daha esnektir. TypeScript'in kendi dokümanları da mümkün olduğunda overload yerine union parametreleri önerir. Nedenini tuzaklar bölümünde göreceksin.
this Parametresi
JavaScript serilerinde this'in fonksiyonun nasıl çağrıldığına göre değiştiğini görmüştük. Bir method'u object'ten koparıp tek başına çağırdığında this artık o object değildir. TypeScript bu hatayı yakalayabilir, yeter ki this'in ne olması gerektiğini söyle:
function handleClick(this: { label: string }) {
console.log(`Clicked: ${this.label}`);
}
const button = { label: "Save", handleClick };
button.handleClick(); // Clicked: Save
const detached = button.handleClick;
// Hata: The 'this' context of type 'void' is not assignable to method's 'this' of type '{ label: string; }'.
detached();
this parametresi her zaman listenin başına yazılır ve gerçek bir parametre değildir: fonksiyonu çağırırken ona argüman vermezsin, JavaScript'e derlenirken de tamamen silinir. Tek işi, fonksiyonun hangi this ile çağrılması gerektiğini derleyiciye anlatmaktır.
Annotation yazmazsan ne olur? strict ailesinin bir parçası olan noImplicitThis açık olduğu için, tipi belli olmayan bir this kullandığında TypeScript 'this' implicitly has type 'any' because it does not have a type annotation. der. Class'ların ve object method'larının içinde this'in tipi zaten bilindiği için bu parametreye en çok tek başına tanımlanan fonksiyonlarda ihtiyaç duyarsın.
Sık Düşülen Tuzaklar
Birinci tuzak, optional parametre ile undefined alabilen parametreyi aynı sanmak:
function logA(message?: string) {
console.log(message ?? "(empty)");
}
function logB(message: string | undefined) {
console.log(message ?? "(empty)");
}
logA();
// Hata: Expected 1 arguments, but got 0.
logB();
İkisinin de içinde message'ın tipi string | undefined. Fark çağıran tarafta: logA'yı argümansız çağırabilirsin, logB'ye ise açıkça bir şey, gerekirse undefined vermen gerekir. Ne istediğine göre seç.
İkinci tuzak, overload'lara union vermek:
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]) {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
function parseAny(input: string | string[]) {
// Hata: No overload matches this call.
return parse(input);
}
Elinde bir string | string[] var ve implementation da tam olarak bunu kabul ediyor. Ama implementation dışarıdan görünmez ve overload'ların hiçbiri tek başına bu union'ı kabul etmez. Ya üçüncü bir overload eklersin ya da baştan union parametreli tek bir imza yazarsın. Overload'lar arttıkça bu tür sürtünmeler de artar.
Üçüncü tuzak, fonksiyon tipi olarak Function kullanmak. const fn: Function = () => 1; derlenir, ama fn'i istediğin sayıda argümanla çağırabilirsin ve dönüş tipi any olur. Yani tip denetimini fiilen kapatmış olursun. Bunun yerine her zaman fonksiyonun gerçek şeklini yaz: () => number.
Küçük Bir Deneme
★☆☆ greet fonksiyonunda greeting parametresini name'in önüne al ve hata mesajını oku. Sonra greeting'e "Hello" default değerini ver; artık ?? operatörüne ihtiyacın kalıyor mu?
★★☆ Derlenir mi, tahmin et: const toLabel: Formatter = (value: string) => value;. Peki const toLabel: Formatter = (value) => value;? İki hata mesajını karşılaştır.
★★☆ parseAny örneğindeki hatayı, parse'a üçüncü bir overload ekleyerek düzelt. parseAny'nin dönüş tipi ne oldu?
★★★ Bir Counter tipi yaz: hiç argüman almadan çağrıldığında bir sonraki sayıyı döndürsün ve reset(): void diye bir method'u olsun. Call signature kullan, sonra bu tipe uyan bir fonksiyon yaz.
Bu yazıda bir fonksiyon tipine, beklediğinden daha az parametre alan bir fonksiyonun atanabildiğini gördük. TypeScript'in tipleri isimlerine göre değil, şekillerine göre karşılaştırmasının sadece bir örneği bu. Bir sonraki yazıda bu fikrin kendisine, yani structural typing'e ve onunla yakından ilgili intersection'lara bakacağız: hangi tipin hangisine neden atanabildiğini sezgisel olarak kavramaya çalışacağız.