Gerçek Bir Örnekle Gelişmiş TypeScript (Bölüm 2)
Yayınlanan: 2020-11-20TypeScript eğitimimize devam etmenin (ve umarım bitirmenin) zamanı geldi. TypeScript hakkında yazdığımız önceki gönderileri kaçırdıysanız, işte bunlar: TypeScript'e ilk girişimiz ve birlikte çalıştığımız JavaScript örneğini ve kısmen geliştirmek için attığımız adımları açıkladığım bu eğitimin ilk bölümü .
Bugün hala eksik olan her şeyi tamamlayarak örneğimizi bitireceğiz. Spesifik olarak, önce diğer mevcut türlerin kısmi versiyonları olan türlerin nasıl oluşturulacağını göreceğiz. Daha sonra tip birleşimlerini kullanarak bir Redux mağazasının eylemlerinin nasıl doğru bir şekilde yazılacağını göreceğiz ve tip birliklerin sunduğu avantajları tartışacağız. Ve son olarak, dönüş tipi argümanlarına bağlı olan bir polimorfik fonksiyonun nasıl oluşturulacağını göstereceğim.
Şimdiye Kadar Yaptıklarımıza Kısa Bir Bakış…
Eğitimin ilk bölümünde, çalışma örneğimiz olarak Nelio Content'ten aldığımız bir Redux mağazasını kullandık (bir parçası). Her şey, onu daha sağlam ve anlaşılır kılan somut türler ekleyerek iyileştirilmesi gereken sade JavaScript kodu olarak başladı. Böylece, örneğin, aşağıdaki türleri tanımladık:
type PostId = number; type Day = string; type Post = { id: PostId; title: string; author: string; day: Day; status: string; isSticky: boolean; }; type State = { posts: Dictionary<PostId, Post>; days: Dictionary<Day, PostId[]>; }; bu, mağazamızın çalıştığı bilgi türünü bir bakışta anlamamıza yardımcı oldu. Bu özel örnekte, örneğin, uygulamamızın durumunun iki şeyi sakladığını görebiliriz: bir posts listesi ( PostId aracılığıyla dizine eklediğimiz) ve belirli bir gün verildiğinde, bir liste döndüren days adlı bir yapı. posta tanımlayıcıları. Bir Post nesnesinde bulacağımız nitelikleri (ve belirli türlerini) de görebiliriz.
Bu tipler tanımlandıktan sonra, örneğimizin tüm fonksiyonlarını onları kullanacak şekilde düzenledik. Bu basit görev, JavaScript'in opak işlev imzalarını dönüştürdü:
// Selectors function getPost( state, id ) { ... } function getPostsInDay( state, day ) { ... } // Actions function receiveNewPost( post ) { ... } function updatePost( postId, attributes ) { ... } // Reducer function reducer( state, action ) { ... }kendini açıklayıcı TypeScript işlev imzalarına:
// Selectors function getPost( state: State, id: PostId ): Post | undefined { ... } function getPostsInDay( state: State, day: Day ): PostId[] { ... } // Actions function receiveNewPost( post: Post ): any { ... } function updatePost( postId: PostId, attributes: any ): any { ... } // Reducer function reducer( state: State, action: any ): State { ... } getPostsInDay işlevi, TypeScript'in kodunuzun kalitesini ne kadar artıracağının çok iyi bir örneğidir. JavaScript karşılığına bakarsanız, bu işlevin ne döndüreceğini gerçekten bilmiyorsunuz. Elbette, adı sonuç türünü gösterebilir (belki bir gönderi listesi mi?), ancak emin olmak için işlevin kaynak koduna (ve muhtemelen eylemler ve azaltıcılar da) bakmalısınız (aslında bir listedir) posta kimlikleri). Kişi bu durumu daha iyi adlandırarak iyileştirebilir ( örneğin getIdsOfPostsInDay ), ancak herhangi bir şüpheyi ortadan kaldıracak somut türler gibisi yoktur: PostId[] .
Şimdi, işlerin mevcut durumu hakkında bilgi sahibi olduğunuza göre, geçen hafta atladığımız her şeyi düzeltmenin zamanı geldi. Özellikle, updatePost işlevinin öznitelik attributes yazmamız gerektiğini biliyoruz ve eylemlerimizin sahip olacağı türleri tanımlamamız gerekiyor ( reducer , action özniteliğinin şu anda any türünde olduğunu unutmayın).
Nitelikleri Başka Bir Nesnenin Alt Kümesi Olan Bir Nesne Nasıl Yazılır?
Basit bir şeyle başlayarak ısınalım. updatePost işlevi, belirli bir posta kimliğinin belirli özelliklerini güncelleme niyetimizi bildiren bir eylem oluşturur. İşte nasıl göründüğü:
function updatePost( postId: PostId, attributes: any ): any { return { type: 'UPDATE_POST', postId, attributes, }; }ve mağazamızdaki gönderiyi güncellemek için redüktör tarafından eylemin nasıl kullanıldığı aşağıda açıklanmıştır:
function reducer( state: State, action: any ): State { // ... switch ( action.type ) { // ... case 'UPDATE_POST': if ( ! state.posts[ action.postId ] ) { return state; } const post = { ...state.posts[ action.postId ], ...action.attributes, }; return { ... }; } // ... }Gördüğünüz gibi, indirgeyici mağazadaki gönderiyi arar ve oradaysa, eylemde bulunanları kullanarak üzerine yazarak özelliklerini günceller.
Ancak bir eylemin attributes tam olarak nedir? Bir gönderide bulabileceğimiz niteliklerin üzerine yazmaları gerektiğinden, açıkça bir Post benzeyen bir şeydirler:
type UpdatePostAction = { type: 'UPDATE_POST'; postId: number; attributes: Post; };ama bunu kullanmaya çalışırsak işe yaramadığını göreceğiz:
const post: Post = { id: 1, title: 'Title', author: 'Ruth', day: '2020-10-01', status: 'draft', isSticky: false, }; const action: UpdatePostAction = { type: 'UPDATE_POST', postId: 1, attributes: { author: 'Toni', }, }; çünkü attributes bir Post olmasını istemiyoruz; bunun Post özniteliklerinin bir alt kümesi olmasını istiyoruz (yani, bir Post nesnesinin yalnızca üzerine yazacağımız özniteliklerini belirtmek istiyoruz).
Bu sorunu çözmek için Partial yardımcı program türünü kullanın:
type UpdatePostAction = { type: 'UPDATE_POST'; postId: number; attributes: Partial<Post>; };Ve bu kadar! Yoksa öyle mi?
Nitelikleri Açıkça Filtreleme
TypeScript'in derleyicisinin kontrol etmediği bazı çalışma zamanı hataları almak mümkün olduğundan, önceki kod parçası hala hatalı. İşte nedeni: Bir gönderi kimliği ve güncellemek istediğimiz öznitelikler kümesi olmak üzere iki bağımsız değişkenden oluşan bir güncelleme sonrası sinyal veren eylem. İşlemi hazır hale getirdiğimizde, indirgeyici, mevcut gönderinin üzerine yeni değerlerle yazmaktan sorumludur:
const post = { ...state.posts[ action.postId ], ...action.attributes, }; Kodumuzdaki hatalı kısım da tam olarak bu; eylemin postId özniteliğinin bir kap kimliği x olması ve özniteliklerdeki id attributes farklı bir posta kimliği y olması mümkündür:
const action: UpdatePostAction = { type: 'UPDATE_POST', postId: 1, attributes: { id: 2, author: 'Toni', }, }; Bu açıkça geçerli bir eylemdir ve bu nedenle TypeScript herhangi bir hatayı tetiklemez, ancak olmaması gerektiğini biliyoruz. attributes (varsa) id özniteliği ve postId özniteliği aynı değere sahip olmalıdır, yoksa tutarsız bir eylemimiz olur. Eylem tipimiz kesin değil çünkü imkansız olması gereken bir durumu tanımlamamıza izin veriyor… peki bunu nasıl düzeltebiliriz? Oldukça kolay: sadece bu türü değiştirin, böylece imkansız olması gereken bu senaryo gerçekten imkansız hale gelir.
Aklıma gelen ilk çözüm şuydu: postId niteliğini kaldırın ve kimliği attributes niteliğine ekleyin:
type UpdatePostAction = { type: 'UPDATE_POST'; attributes: Partial<Post>; }; function updatePost( postId: PostId, attributes: Partial<Post> ): UpdatePostAction { return { type: 'UPDATE_POST', attributes: { ...attributes, id: postId, }, }; } Ardından, indirgeyicinizi, mevcut gönderiyi bulmak ve üzerine yazmak için action.attributes.id yerine action.postId kullanacak şekilde güncelleyin.
Ne yazık ki, bu çözüm ideal değil, çünkü attributes "kısmi bir gönderi", hatırladınız mı? Bu, teorik olarak, id niteliğinin, attributes nesnesinde olabileceği veya olmayabileceği anlamına gelir. Elbette, orada olacağını biliyoruz, çünkü eylemi yaratan biziz… ama türlerimiz hala belirsiz. Gelecekte birisi updatePost işlevini değiştirirse ve attributes postId içerdiğinden emin olmazsa, ortaya çıkan eylem TypeScript'e göre geçerli olur, ancak kodumuz çalışmaz:
const workingAction: UpdatePostAction = { type: 'UPDATE_POST', attributes: { id: 1, author: 'Toni', }, }; const failingAction: UpdatePostAction = { type: 'UPDATE_POST', attributes: { author: 'Toni', }, };Bu nedenle, TypeScript'in bizi korumasını istiyorsak, türleri belirlerken mümkün olduğunca kesin olmalı ve imkansız durumları imkansız kıldıklarından emin olmalıyız. Tüm bunları göz önünde bulundurarak, elimizde sadece iki seçeneğimiz var:
- Eğer eylem halinde bir
postIdözniteliğine sahipsek (başlangıçta yaptığımız gibi), o zamanattributesnesnesi biridözniteliği içermemelidir . - Öte yandan, eylemin bir
postIdözniteliği yoksa,attributesbiridözniteliği içermesi gerekir .
İlk çözüm, mevcut bir türden öznitelikleri kaldırarak yeni bir tür oluşturmamıza izin veren başka bir yardımcı program türü olan Omit kullanılarak kolayca belirtilebilir:
type UpdatePostAction = { type: 'UPDATE_POST'; postId: PostId, attributes: Partial< Omit<Post, 'id'> >; };hangi beklendiği gibi çalışır:
const workingAction: UpdatePostAction = { type: 'UPDATE_POST', postId: 1, attributes: { author: 'Toni', }, }; const failingAction: UpdatePostAction = { type: 'UPDATE_POST', postId: 1, attributes: { id: 1, author: 'Toni', }, }; İkinci seçenek için, tanımladığımız Partial<Post> türünün üstüne id niteliğini açıkça eklemeliyiz:

type UpdatePostAction = { type: 'UPDATE_POST'; attributes: Partial<Post> & { id: PostId }; };bu da bize beklenen sonucu verir:
const workingAction: UpdatePostAction = { type: 'UPDATE_POST', attributes: { id: 1, author: 'Toni', }, }; const failingAction: UpdatePostAction = { type: 'UPDATE_POST', attributes: { author: 'Toni', }, };Birlik Türleri
Bir önceki bölümde, mağazamızın sahip olduğu iki işlemden birinin nasıl yazılacağını zaten gördük. Aynı işlemi ikinci işlem için de yapalım. receiveNewPost şöyle göründüğünü bilmek:
function receiveNewPost( post: Post ): any { return { type: 'RECEIVE_NEW_POST', post, }; }türü şu şekilde tanımlanabilir:
type ReceiveNewPostAction = { type: 'RECEIVE_NEW_POST'; post: Post; };Kolay değil mi?
Şimdi redüktörümüze bir göz atalım: bir state ve bir action alır (türünü henüz bilmediğimiz) ve yeni bir State üretir:
function reducer( state: State, action: any ): State { ... } Mağazamızda iki farklı eylem türü vardır: UpdatePostAction ve ReceiveNewPostAction . Öyleyse action argümanının türü nedir? Biri ya da diğeri, değil mi? Bir değişken birden fazla A , B , C vb. türünü kabul edebiliyorsa, türü bir tür birleşimidir. Yani, türü A veya B veya C olabilir vb. Birleşim türleri, değerleri bu birleşimde belirtilen türlerden herhangi birinde olabilen bir türdür.
Action türümüzün bir birlik türü olarak nasıl tanımlanabileceği aşağıda açıklanmıştır:
type Action = UpdatePostAction | ReceiveNewPostAction; Önceki pasaj, yalnızca bir Action UpdatePostAction türünün bir örneği veya ReceiveNewPostAction türünün bir örneği olabileceğini belirtir.
Şimdi redüktörümüzde Action kullanırsak:
function reducer( state: State, action: Action ): State { ... }kod tabanlı, iyi yazılmış bu yeni sürümünün nasıl sorunsuz çalıştığını görebiliriz.
Sendika Türleri Varsayılan Vakaları Nasıl Ortadan Kaldırır?
“Bir saniye,” diyebilirsiniz, “önceki bağlantı düzgün çalışmıyor, derleyici bir hatayı tetikliyor!” Gerçekten de TypeScript'e göre redüktörümüz erişilemeyen kod içeriyor:
function reducer( state: State, action: Action ): State { // ... switch ( action.type ) { case 'RECEIVE_NEW_POST': // ... case 'UPDATE_POST': // ... } return state; //Error! Unreachable code }Bir dakika ne? Burada neler olduğunu açıklayayım…
Oluşturduğumuz Action union tipi aslında ayrımcı bir union tipidir. Ayrımcı bir birlik türü, tüm türlerinin değeri bir türü diğerinden ayırt etmek için kullanılabilen ortak bir özniteliği paylaştığı bir birlik türüdür.
Bizim durumumuzda, iki Action türünün değerleri UPDATE_POST için RECEIVE_NEW_POST ve UpdatePostAction için ReceiveNewPostAction olan bir type özniteliğine sahiptir. Bir Action zorunlu olarak bir eylemin ya da diğerinin bir örneği olduğunu bildiğimiz için, switch iki dalı tüm olasılıkları kapsar: ya action.type RECEIVE_NEW_POST ya da UPDATE_POST . Bu nedenle, nihai return gereksizdir ve ulaşılamaz olacaktır.
Öyleyse, bu hatayı düzeltmek için bu return kaldırdığımızı varsayalım. Gereksiz kodu kaldırmanın ötesinde bir şey kazandık mı? Cevap Evet. Şimdi kodumuza yeni bir eylem türü eklersek:
type Action = | UpdatePostAction | ReceiveNewPostAction | NewFeatureAction; type NewFeatureAction = { type: 'NEW_FEATURE'; // ... }; birdenbire redüktörümüzdeki switch ifadesi artık tüm olası senaryoları kapsamayacaktır:
function reducer( state: State, action: Action ): State { // ... switch ( action.type ) { case 'RECEIVE_NEW_POST': // ... case 'UPDATE_POST': // ... // case NEW_FEATURE is missing... } // return undefined is now implicit } Bu, NEW_FEATURE türünde bir eylem kullanarak onu çağırırsak, redüktörün örtük olarak undefined bir değer döndürebileceği anlamına gelir ve bu, işlevin imzasıyla eşleşmeyen bir şeydir. Bu uyumsuzluk nedeniyle TypeScript şikayet eder ve bu yeni eylem türüyle başa çıkmak için yeni bir dalın eksik olduğunu bize bildirir.
Değişken Dönüş Tipli Polimorfik Fonksiyonlar
Buraya kadar geldiyseniz tebrikler: TypeScript kullanarak JavaScript uygulamalarınızın kaynak kodunu geliştirmek için yapmanız gereken her şeyi öğrendiniz. Ve ödül olarak birkaç gün önce karşılaştığım bir “problemi” ve çözümünü sizlerle paylaşacağım. Niye ya? Çünkü TypeScript karmaşık ve büyüleyici bir dünya ve bunun ne kadar doğru olduğunu size göstermek istiyorum.
Tüm bu maceranın başlangıcında, sahip olduğumuz seçicilerden birinin getPostsInDay olduğunu ve dönüş türünün nasıl posta kimliklerinin bir listesi olduğunu gördük:
function getPostsInDay( state: State, day: Day ): PostId[] { return state.days[ day ] ?? []; }adından da anlaşılacağı gibi, bir gönderi listesi döndürebilir. Neden böyle yanıltıcı bir isim kullandım, merak ediyorsunuz? Pekala, şu senaryoyu hayal edin: bu işlevin, argümanlarından birinin değerine bağlı olarak, posta kimliklerinin bir listesini veya gerçek gönderilerin bir listesini döndürmesini istediğinizi varsayalım. Bunun gibi bir şey:
const ids: PostId[] = getPostsInDay( '2020-10-01', 'id' ); const posts: Post[] = getPostsInDay( '2020-10-01', 'all' );Bunu TypeScript'te yapabilir miyiz? Tabii ki yaparız! Aksi halde bunu neden gündeme getireyim? Tek yapmamız gereken sonucu girdi parametrelerine bağlı olan bir polimorfik fonksiyon tanımlamak.
Yani fikir şu ki, aynı fonksiyonun iki farklı versiyonunu istiyoruz. Özniteliklerden biri id string , PostId s'nin bir listesi döndürülmelidir. Aynı öznitelik all string , diğeri Post s listesini döndürmelidir.
İkisini de oluşturalım:
function getPostsInDay( state: State, day: Day, mode: 'id' ): PostId[] { // ... } function getPostsInDay( state: State, day: Day, mode: 'all' ): Post[] { // ... }Kolay değil mi? YANLIŞ! Bu işe yaramaz. TypeScript'e göre, "yinelenen işlev uygulamamız" var.
Tamam, o zaman farklı bir şey deneyelim. Önceki iki tanımı tek bir fonksiyonda birleştirelim:
function getPostsInDay( state: State, day: Day, mode: 'id' | 'all' = 'id' ): PostId[] | Post[] { if ( 'id' === mode ) { return state.days[ day ] ?? []; } return []; }Bu bizim istediğimiz gibi mi davranıyor? Korkarım öyle değil…
İşte bu fonksiyon imzasının bize söylediği şey: “ getPostsInDay , değerleri id veya all olabilen bir state ve bir mode olmak üzere iki argüman alan bir fonksiyondur; dönüş türü, PostId s listesi veya Post s listesi olacaktır.” Başka bir deyişle, önceki işlev tanımı, mode bağımsız değişkenine verilen değer ile işlevin dönüş türü arasında bir ilişki olduğunu hiçbir yerde belirtmez. Ve böylece şöyle kodlayın:
const state: State = { posts: {}, days: {} }; const ids: PostId[] = getPostsInDay( state, '2020-10-01', 'id' ); const posts: Post[] = getPostsInDay( state, '2020-10-01', 'all' );geçerlidir ve bizim istediğimiz gibi davranmaz.
Tamam, son deneme. Somut fonksiyon imzalarını tanımladığımız ilk sezgimizi tek, geçerli bir uygulama ile karıştırırsak ne olur?
function getPostsInDay( state: State, day: Day, mode: 'id' ): PostId[]; function getPostsInDay( state: State, day: Day, mode: 'all' ): Post[]; function getPostsInDay( state: State, day: Day, mode: 'id' | 'all' ): PostId[] | Post[] { const postIds = state.days[ day ] ?? []; if ( 'id' === mode ) { return postIds; } return postIds .map( ( pid ) => getPost( state, pid ) ) .filter( ( p ): p is Post => !! p ); } Önceki parçanın, çalışan geçerli bir işlev uygulaması vardır, ancak mode somut değerleri işlevin dönüş türüyle bağlayan iki ekstra işlev imzası tanımlar.
Bu yaklaşımı kullanarak, bu kod geçerlidir:
const ids: PostId[] = getPostsInDay( state, '2020-10-01', 'id' ); const posts: Post[] = getPostsInDay( state, '2020-10-01', 'all' );ve bu yapmaz:
const ids: PostId[] = getPostsInDay( state, '2020-10-01', 'all' ); const posts: Post[] = getPostsInDay( state, '2020-10-01', 'id' );Sonuçlar
Bu yazı dizisinde TypeScript'in ne olduğunu ve projelerimizde nasıl uygulayabileceğimizi gördük. Türler, anlamsal bağlam sağlayarak kodu daha iyi belgelememize yardımcı olur. Ayrıca, TypeScript derleyicisi, tıpkı Legos'un yaptığı gibi, kodumuzun birbirine doğru şekilde uyduğunu doğrulamakla ilgilendiğinden, türler ayrıca ekstra bir güvenlik katmanı ekler.
Bu noktada, işinizin kalitesini bir sonraki seviyeye taşımak için gerekli tüm araçlara zaten sahipsiniz. Bu yeni macerada iyi şanslar!
Unsplash'ta Mike Kenneally tarafından öne çıkan görsel.
