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.

JavaScript
let language = "en"
Rust
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.

Bash
npm install -g typescript
tsc index.ts

Bu 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

TypeScript
let city: string = "Lyon"
let isCapital: boolean = false
let population: number = 520774

Bu ş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.

TypeScript
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.

TypeScript
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.

TypeScript
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:

TypeScript
type City = {
  id: number
  name: string
  population: number
  isCapital: boolean
  location: {
    latitude: number
    longitude: number
  }
}

ayrı veri tipi şeklinde:

TypeScript
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.

TypeScript
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.

TypeScript
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:

TypeScript
const cities: City[] = [city1, city2]

veya bir başka gösterimle:

TypeScript
const cities: Array<City> = [city1, city2]

Birleşim tipleri

Değer türleri sadece belli değerler alabilen veri tipidir.

TypeScript
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.

TypeScript
const addNewCity = (data): City => {
  const newCity: City = {
    id: lastCityId,
    ...data
  }
  cities.push(newCity)
  return newCity
}

Dönüş tipini de birden fazla tip olarak belirleyebiliriz.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
type CityUpdateData = Partial<City>

Bu tanımlama City içerisindeki her alanı isteğe bağlı hale getirir.

Direkt örnek üzerinde de tanımlayabiliriz:

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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

  1. 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.
  2. 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.
  3. 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.
  4. 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;

JavaScript
// @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.

TypeScript
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.

TypeScript
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.

TypeScript
type User = z.infer<typeof UserSchema>

Bu sayede User tipimiz aşağıdaki gibi gözükür:

TypeScript
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.