Javascript ile uzun süredir ilgilenip Typescript’in ne olduğunu bilmeyenlere veya bir türlü kullanmak istemeyenlere özel böyle bir hızlandırılmış Typescript rehberi hazırlamak istedim. Her şeyden önce Typescript neden var sorusuyla başlayacağız. Ama önce size programlama dillerinin tarihi kadar eski bir ikilemi anlatayım.
Statik tipli diller & Dinamik tipli diller
Herhangi bir programlama dilini öğrenirken en başta o programlama dilinin statik tipli mi yoksa dinamik tipli mi olduğu anlatılır. Statik tipli dillerde oluşturduğunuz değişkenin veri tipini özellikle yazmanız gerekir. Örnek olarak Java, C++, Go, Rust ve bu yazıda anlatacağımız Typescript statik programlama dilleridir. Dinamik tipli dillerde ise değişkenin veri tipi çalıştırılma anında hesaplanır. Bu sebeple de oluşturduğumuz değişkenlere tip belirtmek zorunda kalmayız. Python, PHP, Ruby, Javascript gibi diller dinamik tipli dillerdir. Aslında günümüzde programlama dilleri için bu kadar net ayrım yapmamız doğru olmayabilir. Çünkü dinamik olarak belirtilen dillere zamanla statik işaretleme özellikleri getirildi. Tam tersi şekilde de Typescript’te bulunan any ve unknown gibi tipler nedeniyle bu dili “gradually typed” olarak sınıflandırmak daha doğru. Ama şimdilik bunlarla kafamızı bulandırmayalım. Bu ikisinin farkını kod üzerinde görelim.
let language = "en"let language: &str = "en";Gördüğünüz gibi Rust statik tip tanımına sahip olduğundan oluşturduğumuz değişkenin tipini özellikle belirtiyoruz. Ancak Javascript dilinde ise değişkenin tipini belirlememize gerek yok. Peki neden böyle bir fark var? Avantajı ve dezavantajı nedir?
Öncelikle dinamik tipli bir dil size daha cazip göründüyse bunun sebebi gayet anlaşılır. Yazması kesinlikle daha kolay. Ancak kodun bir ekip tarafından yazılacağı düşünüldüğünde bazı şeylerin açıkça yazılması hem ekip için en başta da yazılımın kendisi için büyük bir avantaj oluşturuyor. Hatta kendi yazdığınız kodu bile aradan zaman geçtikçe unuttuğunuz için tek başına yazdığınız kodu ekip kodu olarak adlandırabiliriz. Zamanla yazdığınız kodu unutacak, kodun bir kısmında bir değişiklik yapacaksınız ve bir de bakmışsınız daha önce çalışan kodunuz artık çalışmıyor. İşte statik tipli dillerin bu noktada kesinlikle bir avantajı var.
Ancak yine de bu bir tercih meselesi. Bunu unutmamak gerekiyor. Dahil olduğunuz projenin büyüklüğünden tutun da kodun karmaşıklığına, ekibin büyüklüğüne ve yapılan işte özelleşmiş programlama dillerine kadar bu tercihi etkileyen birçok şey var. İyi bir yazılımcı olabilmek için bu farkı iyi anlamalı, gerektiği yerde statik bir dil kullanmanın avantajlarını göz ardı etmemelisiniz.
Typescript’in doğuşu
Bildiğiniz gibi Javascript dinamik tipli bir dil. 2012 yılında Microsoft ekibi tarafından hızla büyüyen Javascript projelerinde ortaya çıkan klasik hataların önüne geçebilmek için Typescript projesi tanıtıldı. Javascript’in kullandığı .js uzantısı yerine .ts uzantılı dosyalara statik tip tanımlamalı bir Javascript yazabiliyordunuz. En başta popülaritesi çok fazla değilken 2016 yılında Angular’ın 2. sürümünde varsayılan olarak Typescript kullanılmasıyla popülerlik kazandı. Günümüzde özellikle npm ekosistemi için yazılan eklentilerde, büyük çaplı projelerde kullanılmakta. Şimdi hızlı bir Typescript rehberi yapalım. Bize kazandırdığı özellikleri öğrenelim. Yazının sonlarına doğru da gerçekten kullanmamıza gerek var mı, alternatif olarak ne kullanabiliriz gibi soruları tartışalım.
.ts uzantılı dosya çalıştırma
.ts dosyasını derlemek için Typescript derleyicisi kullanılır. Aşağıdaki yöntemle Typescript Compiler’i kurarak dosyanızı derleyebilirsiniz.
npm install -g typescript
tsc index.tsBu kod dosyanızdaki hataları tespit edip, hata yoksa kodunuzu javascript’e derleyecektir. Visual Studio Code kullanıyorsanız “Error Lens” eklentisiyle build etmeden de bu hataları dosyanızda anlık olarak gözlemleyebilirsiniz.
Değişken tipi tanımlama
let city: string = "Lyon"
let isCapital: boolean = false
let population: number = 520774Bu şekilde değişkenlerimize tip tanımlaması yapabiliyoruz. değişkenlerimizin değerlerini ilerde olmaması gereken bir tipe eşitlemeye çalıştığımızda hata alacağız.
Aynı şekilde fonksiyon içerisindeki parametreye de tip tanımlayabiliyoruz.
const changePopulation = (cityId: number, newPopulation: number) => { }Özel veri tipi tanımlama
Typescript kullanmanın en iyi avantajlarından birisi de kendi özel veri tipinizi tanımlayabilmeniz. Bu sayede verilerin yönetimi daha kolay bir hale geliyor. Bir örneğe bakalım.
const city1 = {
id: 1,
name: "Lyon",
population: 520774,
isCapital: false
}Bu şekilde oluşturulan objeye bakarak bir şehir tanımlamasında nelerin gerektiğini anlamamız zor. Belki eklememiz gereken bir veri vardı ama unuttuk. Bunu nasıl bilebiliriz? İşte tip tanımlamasını en başta yaparsak ve sınırlarını belirlersek bu karmaşadan kurtulabiliriz.
type City = {
id: number
name: string
population: number
isCapital: boolean
}
const city1: City = {
id: 1,
name: "Lyon",
population: 520774,
isCapital: false
}İç içe veri tipi tanımlama
İç içe olan veri tiplerini normal obje tanımlaması yapar gibi veya yeni bir veri tipi tanımlayarak yapabiliriz.
obje tanımlaması şeklinde:
type City = {
id: number
name: string
population: number
isCapital: boolean
location: {
latitude: number
longitude: number
}
}ayrı veri tipi şeklinde:
type Location = {
latitude: number
longitude: number
}
type City = {
id: number
name: string
population: number
isCapital: boolean
location: Location
}Zorunlu olmayan özellikler
Üstteki örneğimizde verilen location bilgisi bazı verilerde bulunmayabileceğini düşünelim. Bunun isteğe bağlı olduğunu belirtmek için ? işaretini kullanırız.
type Location = {
latitude: number
longitude: number
}
type City = {
id: number
name: string
population: number
isCapital: boolean
location?: Location
}
const city1 = {
id: 1,
name: "Lyon",
population: 520774,
isCapital: false
}
const printCityLocation = (city: City) => {
console.log(`${city.name} is located at: ${city.location.latitude}, ${city.location.longitude}`)
}
Location özelliğinin isteğe bağlı olduğunu belirttiğimiz için buna direkt erişmeye çalıştığımızda “TypeError: Cannot read properties of undefined (reading ‘location’)” hatası alacağız. Bunu engellemek için de latitude ve longitude özelliklerine erişmeye çalışırken yine ? işaretini kullanarak isteğe bağlı erişim yaptığımızı belirtmeliyiz.
const printCityLocation = (city: City) => {
console.log(`${city.name} is located at: ${city.location?.latitude}, ${city.location?.longitude}`)
}
Neden böyle eriştiğimizi anlamadıysanız optional chaining konusuna bakabilirsiniz.
Array tipi tanımlama
Şimdi City veri tipini taşıyan birçok objeyi array içerisinde tuttuğumuzu düşünelim. Bunun için veri tipini şu şekilde tanımlarız:
const cities: City[] = [city1, city2]veya bir başka gösterimle:
const cities: Array<City> = [city1, city2]Birleşim tipleri
Değer türleri sadece belli değerler alabilen veri tipidir.
type City = {
id: number
name: string
population: number
isCapital: boolean
location?: Location
coastalStatus: "Coastal" | "Inland"
}
const city1 = {
id: 1,
name: "Lyon",
population: 520774,
isCapital: false,
coastalStatus: "Inland"
}Bu sayede coastalStatus özelliğinin sadece iki parametre alabilmesini sağladık. Bunlar dışında bir değer girdiğimizde hatayla karşılaşacağız.
Fonksiyon dönüş tipi
Fonksiyonlarınıza dönüş tipi tanımlayabilirsiniz. Bu sayede fonksiyonu kullandığınızda derleyici dönüş tipini bildiği için olası hatalarda sizi uyarabilecektir.
const addNewCity = (data): City => {
const newCity: City = {
id: lastCityId,
...data
}
cities.push(newCity)
return newCity
}Dönüş tipini de birden fazla tip olarak belirleyebiliriz.
const getCityDetail = (cityId: number): City | undefined => {
return cities.find(item => item.id === cityId)
}Eğer fonksiyon geriye bir şey döndürmüyorsa “void” tipini kullanabiliriz.
const logCity = (city: City): void => {
console.log(`city name: ${city.name}`)
}Omit kullanımı
addNewCity fonksiyonunda data parametresine bir tip tanımlaması yapmadık. Eğer City şeklinde tip tanımlaması yaparsak sorun olacak. Çünkü gönderdiğimiz data içerisinde id yok, bunu fonksiyon içerisinde ekletiyoruz. Yani tip tanımlaması yapabilmemiz için City tipinin kopyası olan ama içinde id içermeyen bir tip tanımlamamız gerekiyor. Kopyalamak yerine Omit kullanarak bunun üstesinden gelebiliriz. Omit ile şu hariç şeklinde tanımlama yapabiliyoruz.
const addNewCity = (data: Omit<City, "id">): City => {
const newCity: City = {
id: lastCityId,
...data
}
cities.push(newCity)
return newCity
}Birden fazla özelliği hariç tutmak istersek | karakteriyle birleştirebiliriz.
const addNewCity = (data: Omit<City, "id" | "name">): City => { }Partial kullanımı
Şimdi şehirleri güncelleyen bir fonksiyon yazalım. Partial tanımına neden ihtiyaç duyduğumuzu daha iyi anlayacağız.
const updateCity = (cityId: number, data): City => {
const foundCity = cities.find(item => item.id === cityId)
if (!foundCity) {
throw new Error(`City with ID ${cityId} was not found`)
}
Object.assign(foundCity, data)
return foundCity
}Kodumuzda tek bir sorun var. data objesine bir tip ataması yapmadık. data objesi içerisinde herhangi bir alan gönderilebilir. sadece bir tane de değil birden fazla alanı aynı anda güncelleyebiliriz. Yani aslında ihtiyacımız olan şeyi City tip tanımlamasındaki her şeyi kopyalayıp isteğe bağlı hale getirerek yapabiliriz.
type CityUpdateData = {
id?: number
name?: string
population?: number
isCapital?: boolean
location?: Location
coastalStatus?: "Coastal" | "Inland"
}Böylece sorunumuz çözülmüş oluyor. Ama bu tanımlayıp yapmanın daha kolay bir yolu var.
type CityUpdateData = Partial<City>Bu tanımlama City içerisindeki her alanı isteğe bağlı hale getirir.
Direkt örnek üzerinde de tanımlayabiliriz:
const updateCity = (cityId: id, data: Partial<City>): City => {
const foundCity = cities.find(item => item.id === cityId)
if (!foundCity) {
throw new Error(`City with ID ${cityId} was not found`)
}
Object.assign(foundCity, data)
return foundCity
}Generics
Şöyle bir örnek düşünelim şimdi. cities dizisine verdiğimiz city objesini ekleyen aynı şekilde towns dizisine de verdiğimiz town objesini ekleyen bir fonksiyonumuz olsun. Bunun ismini addPlaceToArray yapalım.
const addPlaceToArray = (array, item) => {
array.push(item)
return array
}
addPlaceToArray(cities, {
id: 1,
name: "Lyon",
population: 520774,
isCapital: false,
coastalStatus: "Inland"
})
addPlaceToArray(towns, {
id: 1,
name: "Annecy",
population: 131481
})Bu örnekte tip tanımlaması yapamadığımızı fark etmiş olmalısınız. Çünkü addPlaceToArray değişkeni çoklu tip tanımlaması alıyor. ama aldığı parametreler birbiriyle bağlantılı. Bunun gibi durumlarda Generics kullanıyoruz.
const addPlaceToArray = <T>(array: T[], item: T): T[] => {
array.push(item)
return array
}Burda verdiğimi T isimlendirmesinin bir önemi yok, istediğiniz isimlendirmeyi verebilirsiniz. T harfi kısa olduğundan yaygın olarak kullanılmaktadır.
Bu haliyle bırakırsak addPlaceToArray fonksiyonunu çağırırken verdiğimiz data düzgün bir şekilde tip tanımına girmez. Bunu da düzeltmek için fonksiyonu çağırdığımız yerde de tip tanımlamasını belirtmeliyiz.
addPlaceToArray<Town>(towns, {
id: 1,
name: "Annecy",
population: 131481
})Bu sayede derleyici verdiğimiz datanın Town tipine ait olduğunu anlayacaktır.
İlerisi…
Şu ana kadar Typescript’in en başlıca özelliklerini tanıttım. Bunları aslında benim tanıtmama da gerek yoktu. Typescript’in kendi dökümanında açık bir şekilde anlatmışlar. Buradan göz atabilirsiniz. Önemli olan aslında burdaki felsefeyi kavramanız. Typescript ile kod yazma pratiği aslında kodunuzda tam olarak ne yapmaya çalıştığınızı da daha iyi anlamanızı sağlayacak. İşleri soyuttan somuta çeviren bir araç gibi düşünebilirsiniz. Fonksiyonun alacağı parametreler, bu parametrelerin tipi, fonksiyonun dönüş tipi gibi tanımlamalar belirli olduğunda bunların arasında yazacağınız mantığı oturtmanız da daha kolay olacak.
Büyük firmalar neden Typescript kullanmayı bırakıyor?
Bu videoda denk geldiğimde biraz şaşırmakla beraber aslında olayların bu noktaya geleceğini de tahmin edebiliyordum. Çünkü her ne kadar avantajlarını saysak da yaptığınız projeye göre dezavantajları avantajlarını geçebilir. Evet Typescript bize daha hatalardan arınmış kod sağlasa da kodu karmaşıklaştırdığı, ekipteki her birey için ekstra bir öğrenme gereksinimi kattığı ortada. Videoyu tamamen izlediyseniz tabi ki bu firmaların tamamen bu felsefeden soyutlanmadığını görmüşsünüzdür. Aslında Typescript’i bırakarak yerine koydukları başka çözümler var. Öncelikle dezavantajları görelim.
Dezavantajlar
- Büyük projelerde build işlemleri çok uzun sürüyor. Ancak şöyle bir çözüm getirilebilir. Build işlemini her seferinde çağırmaktansa tsc –noEmit kullanarak sadece hata ayıklaması yapabiliriz. Yani build işlemini sadece çalışma sonunda alırız, kod değişikliklerinde sadece hataları kontrol eden kodu çağırırız. Böylece uzun süren build işleminin sayısını azaltmış oluruz.
- Runtime esnasında API’dan çekilen verilerle belirlediğimiz veriler uyuşmuyor. Typescript çalışma anındaki uyuşmazlıkları yakalayamıyor, sadece derleme aşamasındakileri görebiliyor. Bu sorun için de geliştirilmiş kütüphaneler var. En yaygın kullanılanı “Zod” Bu eklentiyle ilgili de bir paragraf ekleyeceğim.
- Takımdaki herkesin Typescript öğrenmesi gerekiyor. Buna bir yorum olarak da öğrenme eşiğinin fazla olmadığını bu yazıyla da görebilirsiniz. Ama yine de ekstra bir öğrenme gereksinimi var.
- Kod saf Javascript’ten uzaklaşıyor.
Shopify, derleme maliyetini düşürmek için saf JS + JSDoc + TypeScript tip denetimine geçti. Bunun anlamı şu. Kod .js formatında yazılır. JSDoc formatı kullanılarak her kodun üstüne parametre tiplerini, dönüş tiplerini içeren yorumlar eklenir. Bunlar da tip denetleyiciler kullanarak derlemesiz kontrol edilir. Bu işleyiş sayesinde .js formatı kullanarak da tip kontrolü sağlanır. Ama bu işleyiş Typescript kadar gelişmiş bir tip kontrolü sunmaz.
JSDoc formatına örnek olarak;
// @ts-check
/**
* @typedef {Object} City
* @property {number} id
* @property {string} name
*/
/** @type {City[]} */
const cities = [
{ id: 1, name: "Istanbul" },
{ id: 2, name: "Rome" }
];Zod ile runtime sırasında tip kontrolü
Son olarak runtime sırasında tip kontrolü yapmamızı sağlayan kütüphanelerden en yaygın olanına Zod’u inceleyelim.
import * as z from "zod"
const UserSchema = z.object({
userId: z.number(),
id: z.number(),
title: z.string(),
completed: z.boolean()
})Gördüğümüz şekliyle bir schema tanımlayabiliyoruz. Şimdi önemli olan şey API’den gelen verimizi bu şemayla eşleştirebilmek. Bunun için parse fonksiyonunu kullanıyoruz.
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
const data = await response.json()
const user = UserSchema.parse(data)
console.log(user.title.toUpperCase())Bu şekilde runtime sırasında eşleşmeyen veriler için hata verdirerek sorunu en başından tespit edebiliriz. Ayrıca schema ile tanımladığımız veri yapısını typescript’teki type yapısına çevirebiliriz.
type User = z.infer<typeof UserSchema>Bu sayede User tipimiz aşağıdaki gibi gözükür:
type User = {
userId: number;
id: number;
title: string;
completed: boolean;
}Zod kütüphanesini hem front-end hem de back-end tarafında kullanabilirsiniz. Bu sayede validation işlemlerinde de ortak bir arayüz sağlamış olursunuz. Bu kütüphaneyle ilgili kendi sitesinden daha fazla bilgi alabilirsiniz.