2026'da TypeScript ile Vue 3: Tip Güvenli Props, Emits ve Composables

Vue 3 bileşenlerini TypeScript ile tip güvenli hâle getirmenin pratik rehberi: generic defineProps, tuple defineEmits, tiplenmiş composables, defineModel ve InjectionKey ile mülakat soruları.

Vue 3 TypeScript ile tip güvenli props, emits ve composables

2026'da TypeScript ile birlikte kullanılan Vue 3, props, olaylar ve yeniden kullanılabilir mantık genelinde tam statik tip denetimi sağlar. <script setup> sözdizimi ile defineProps ve defineEmits gibi derleyici makrolarının birleşimi, eskiden yalnızca çalışma zamanında geçerli olan nesne tanımlarını derleme zamanında doğrulanan sözleşmelere dönüştürür. Bu rehber, hem üretim kodunda hem de teknik mülakatlarda her Vue geliştiricisinin ihtiyaç duyduğu tip güvenli desenleri ele alır.

Tek satırda tip güvenli props

Vue 3.5+ ile generic defineProps<T>(), prop tiplerini doğrudan bir TypeScript arayüzünden çıkarır — çalışma zamanı type alanları ve PropType dönüşümü olmadan. Reaktif prop destructuring ile birleştiğinde varsayılan değerler aynı satırda tanımlanır: const { size = 'md' } = defineProps<Props>().

defineProps ve TypeScript arayüzleri ile props tipleme

Çalışma zamanı prop bildirimi (props: { title: String }) işe yarar, ancak tip bilgisini tekrarlar ve kesinliği yitirir. Union tipleri, iç içe nesneler ve fonksiyon imzaları temiz bir biçimde ifade edilemez. defineProps'un generic biçimi bu sorunu, şekli bir TypeScript tipinden alarak çözer; Vue derleyicisi bunu otomatik olarak doğru çalışma zamanı bildirimine dönüştürür.

vue
<!-- UserCard.vue -->
<script setup lang="ts">
interface Props {
  userId: number            // required primitive
  name: string
  role: 'admin' | 'member'  // union type, impossible with runtime props
  tags?: string[]           // optional array
  onSelect?: (id: number) => void  // typed callback prop
}

const props = defineProps<Props>()
</script>

<template>
  <article @click="props.onSelect?.(props.userId)">
    <h3>{{ props.name }}</h3>
    <span>{{ props.role }}</span>
  </article>
</template>

role prop'u artık iki string literaliyle sınırlandırılmıştır; bu nedenle role="guest" geçmek derlemede hata verir. TypeScript ile defineProps'a başvurmanın temel nedeni budur: resmi Vue TypeScript rehberi generic biçimi <script setup> projeleri için varsayılan kabul eder.

Varsayılan prop değerleri: withDefaults ile reaktif destructuring karşılaştırması

Yalnızca tipe dayalı props'ların tek başına çalışma zamanı varsayılan mekanizması yoktur. Tarihsel olarak yanıt, makroyu saran ve bir varsayılanlar nesnesini birleştiren withDefaults idi:

vue
<!-- Button.vue -->
<script setup lang="ts">
interface Props {
  variant?: 'primary' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
}

// withDefaults keeps reactivity and applies fallback values
const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'md',
  disabled: false,
})
</script>

Vue 3.5, bugün daha kısa olan reaktif prop destructuring'i kararlı hâle getirdi. defineProps sonucunu destructure edip aynı ifadede bir varsayılan atamak tamamen reaktif kalır — derleyici, arka planda her erişimi yeniden props.x'e yazar.

vue
<!-- Badge.vue -->
<script setup lang="ts">
interface Props {
  label: string
  color?: 'green' | 'red'
  outlined?: boolean
}

// Defaults are declared inline; each variable stays reactive
const { label, color = 'green', outlined = false } = defineProps<Props>()
</script>

<template>
  <span :class="[color, { outlined }]">{{ label }}</span>
</template>
Destructure edilmiş props'ta reaktiflik tuzağı

Destructure edilmiş props, şablonda ve computed içinde reaktif kalır; ancak destructure edilmiş bir değeri doğrudan watch'a veya bir composable'a geçirmek onu yalnızca bir kez okur ve reaktif bağı koparır. Bir ref gerektiğinde bu değeri bir getter içine sarın — watch(() => color, ...) — veya toRef(props, 'color') ile dönüştürün.

Bu, yaygın bir mülakat tuzağıdır: adaylar destructure edilmiş değişkenin düz bir değer olduğunu varsayar; oysa derleyici aslında her okumayı yeniden props.color'a yönlendirmiştir.

defineEmits ile tip güvenli emits

Olaylar, props ile aynı titizliği hak eder. Generic defineEmits, her olay adını ve yükünü bir tuple olarak tanımlayarak üst bileşene otomatik tamamlama, alt bileşene ise doğru argümanların yayıldığına dair derleme zamanı denetimi sağlar.

vue
<!-- SearchInput.vue -->
<script setup lang="ts">
// Tuple syntax: event name -> [ ...payload types ]
const emit = defineEmits<{
  search: [query: string]
  clear: []                       // no payload
  select: [id: number, label: string]  // multiple args
}>()

function onSubmit(value: string) {
  emit('search', value)   // ✅ typed
  // emit('search', 42)   // ❌ compile error: number not assignable to string
}
</script>

Bu tuple biçimi, eski çağrı imzası sözdizimini ((e: 'search', q: string): void) yerini aldı; çünkü daha okunaklıdır ve aşırı yükleme olmadan birden çok olayı destekler. Bir olay veri taşımıyorsa, boş bir tuple [] bunu açıkça belgeler. Tiplenmiş emits'i tiplenmiş props ile eşleştirmek, tüm genel arayüzü çalışma zamanından önce doğrulanabilir bileşenler üretir — Vue Composition API rehberinde ele alınan aynı disiplin.

Yeniden kullanılabilir mantık için composables tipleme

Composables düz fonksiyonlardır, bu yüzden sıradan TypeScript kurallarına uyarlar — ancak birkaç kural onları ergonomik tutar. Çıkarım açık olmadığında Ref tiplerini açıkça döndürün ve bir composable, bir API yanıtı gibi rastgele veriyi sardığında generic kullanın.

useFetch.tstypescript
import { ref, type Ref } from 'vue'

interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<Error | null>
  loading: Ref<boolean>
}

// Generic <T> flows through to the caller's typed data
export function useFetch<T>(url: string): UseFetchReturn<T> {
  const data = ref<T | null>(null) as Ref<T | null>
  const error = ref<Error | null>(null)
  const loading = ref(true)

  fetch(url)
    .then((r) => r.json())
    .then((json: T) => { data.value = json })
    .catch((e: Error) => { error.value = e })
    .finally(() => { loading.value = false })

  return { data, error, loading }
}

Çağrı noktasında generic parametre, data'yı hiçbir ek açıklama olmadan tamamen tiplenmiş hâle getirir:

UserList.vue (script setup)typescript
interface User { id: number; name: string }

// data is Ref<User[] | null> — inferred from the generic
const { data: users, loading } = useFetch<User[]>('/api/users')

Döndürülen nesneyi açıkça tipleme (UseFetchReturn<T>) o birkaç fazladan satıra değer: sözleşmeyi belgeler, iç ref'lerin kazara sızmasını önler ve tüketicilere içe aktarılacak tek bir tip sunar. Argüman aşırı yüklemeleri ve yaşam döngüsüne duyarlı temizlik gibi daha derin composable desenleri için gelişmiş Vue composables rehberine bakın.

Vue.js / Nuxt.js mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

İki yönlü bağlama için defineModel tipleme

Vue 3.4'ten beri kararlı olan defineModel, eski modelValue prop'unu ve update:modelValue emit'ini tek bir yazılabilir ref'e daraltır. Generic parametresi bağlamanın her iki yönünü aynı anda tipler.

vue
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// model is Ref<number>; parent v-model is type-checked as number
const model = defineModel<number>({ required: true })

function increment() {
  model.value++   // writing back updates the parent
}
</script>

<template>
  <input :value="model" type="number" @input="model = Number(($event.target as HTMLInputElement).value)" />
  <button @click="increment">+1</button>
</template>

Adlandırılmış modeller — defineModel<string>('title')v-model:title ile eşlenir ve aynı tiplemeyi alır. Bu, manuel prop-ve-emit deseninin gizlediği tüm bir uyumsuz yük hataları sınıfını ortadan kaldırır.

Şablon ref'lerini ve bileşen örneklerini tipleme

Bir DOM düğümüne veya alt bileşene ref üzerinden erişmek, tiplenmemiş Vue kodunun en sık any'ye düştüğü yerdir. Çözüm, useTemplateRef'i (Vue 3.5+) veya ref'in kendisini eleman tipiyle parametreleştirmektir; böylece özellik erişimi gerçek DOM arayüzüne karşı denetlenir.

vue
<!-- FocusField.vue -->
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'

// The ref is typed as HTMLInputElement | null
const inputRef = useTemplateRef<HTMLInputElement>('field')

onMounted(() => {
  // .focus() and .select() are known because the element type is explicit
  inputRef.value?.focus()
})
</script>

<template>
  <input ref="field" placeholder="Type to search" />
</template>

Bir alt bileşene yapılan referans için InstanceType<typeof Child>, bileşenin genel tipini çıkararak alt bileşenin defineExpose aracılığıyla bildirdiği her şeyi açığa çıkarır. Bu, üstten alta metot çağrılarının tahmin edilmek yerine tamamen denetlenmesini sağlar.

vue
<!-- Parent.vue -->
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Modal from './Modal.vue'

// Ref is typed to Modal's exposed API (e.g. open / close methods)
const modal = useTemplateRef<InstanceType<typeof Modal>>('modal')

function launch() {
  modal.value?.open()   // ✅ checked against Modal's defineExpose
}
</script>

InjectionKey ile tip güvenli provide ve inject

Bileşen ağacı boyunca bağımlılık enjeksiyonu, anahtar tip bilgisini taşımadıkça bu bilgiyi yitirir. InjectionKey<T>, bir değerin tipini anahtarına bağlayan tiplenmiş bir semboldür; böylece provide ve inject manuel dönüşüm olmadan senkron kalır.

theme-key.tstypescript
import type { InjectionKey, Ref } from 'vue'

export interface ThemeContext {
  mode: Ref<'light' | 'dark'>
  toggle: () => void
}

// The key permanently associates the ThemeContext type with this symbol
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
typescript
// provider (script setup) — value must match ThemeContext or it fails to compile
provide(ThemeKey, { mode, toggle })

// consumer — theme is inferred as ThemeContext | undefined
const theme = inject(ThemeKey)
theme?.toggle()

Şekli ThemeContext ile eşleşmeyen bir değer sağlamak, enjeksiyon noktasında bir derleme hatasıdır; bu da uzak bileşenler arasındaki tutarsızlığı üretime ulaşmadan yakalar.

Sık sorulan Vue TypeScript mülakat soruları

Mülakatçılar, adayın derleme zamanı tipleri ile çalışma zamanı davranışı arasındaki sınırı anlayıp anlamadığını yoklar. Sıkça tekrarlanan birkaç soru ve keskin yanıtları:

  • Çalışma zamanı nesne biçimi yerine neden defineProps<T>() tercih edilir? Generic props, çalışma zamanı bildirimlerinin ifade edemediği union tiplerini, fonksiyon imzalarını ve iç içe şekilleri ifade eder ve tekrarlanan tip/çalışma zamanı tanımlarını ortadan kaldırır.
  • Destructure edilmiş props reaktif midir? Vue 3.5+ ile evet, çünkü derleyici erişimi props.x'e yeniden yazar. Ancak watch'a veya bir composable'a geçirilen destructure edilmiş değerler bir kez okunur — bir getter veya toRef kullanın.
  • Yükü olmayan bir olay nasıl tiplenir? Boş bir tuple ile: defineEmits<{ close: [] }>().
  • defineModel neyin yerini alır? modelValue prop'u ve update:modelValue emit çiftini; tek bir tiplenmiş yazılabilir ref hâlinde birleştirir.

Bu konuları gerçek bir soru bankasına karşı çalışmak, mülakatların ödüllendirdiği refleksleri keskinleştirir — Vue composables mülakat modülü tam olarak bu desenleri çalıştırır. Araçlar da önemlidir: tip hatalarının birleştirmeyi engellemesi için CI'da vue-tsc çalıştırın ve composable generic'leri işin içine girdiğinde TypeScript el kitabına başvurun. Editör deneyimi, bu makroları okuyup hataları yerinde gösteren Vue'nun resmi Volar aracıyla sağlanır.

Sonuç

  • Çalışma zamanı bildirimlerinin yakalayamadığı union tiplerini, isteğe bağlı alanları ve callback prop'larını ifade etmek için generic defineProps<Props>() kullanın.
  • Vue 3.5+ ile satır içi varsayılanlar taşıyan reaktif prop destructuring'i tercih edin ve ortak bir varsayılanlar nesnesi daha anlaşılır olduğunda yalnızca withDefaults'a başvurun.
  • Yüklerin hem alt hem üst bileşende derleme zamanında denetlenmesi için olayları defineEmits'in tuple biçimiyle tipleyin.
  • Composable dönüş tiplerini açıkça açıklayın ve çağıranın sağladığı veri tiplerini baştan sona iletmek için generic kullanın.
  • İki yönlü bağlama için defineModel<T>() benimseyin; böylece prop-artı-emit tekrarını tek bir tiplenmiş ref'e daraltırsınız.
  • Tip gerilemelerinin üretime ulaşmak yerine build'i durdurması için sürekli entegrasyonda vue-tsc çalıştırın.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

#Vue 3
#TypeScript
#defineProps
#Composables
#Frontend

Paylaş

İlgili makaleler