Hemen hemen her web uygulaması veri serileştirmesi gerektirir. Bu ihtiyaç şu durumlarda ortaya çıkar:
Çoğu durumda, veri kaybı veya bozulması ciddi sonuçlara yol açabilir; bu da, geliştirme aşamasında mümkün olduğunca çok sayıda hatanın tespit edilmesine yardımcı olan kullanışlı ve güvenli bir serileştirme mekanizmasının sağlanmasını zorunlu hale getirir. Bu amaçlar doğrultusunda, veri aktarım formatı olarak JSON'u ve geliştirme sırasında statik kod kontrolü için TypeScript'i kullanmak uygundur.
TypeScript, JavaScript'in bir üst kümesi olarak hizmet eder; bu, JSON.stringify
ve JSON.parse
gibi işlevlerin kusursuz şekilde kullanılmasını sağlamalıdır, değil mi? Tüm avantajlarına rağmen TypeScript'in JSON'un ne olduğunu ve JSON'da serileştirme ve seri durumdan çıkarma için hangi veri türlerinin güvenli olduğunu doğal olarak anlamadığı ortaya çıktı.
Bunu bir örnekle açıklayalım.
Örneğin, bazı verileri LocalStorage'a kaydeden bir işlevi düşünün. LocalStorage nesneleri depolayamadığından burada JSON serileştirmesini kullanıyoruz:
interface PostComment { authorId: string; text: string; updatedAt: Date; } function saveComment(comment: PostComment) { const serializedComment = JSON.stringify(comment); localStorage.setItem('draft', serializedComment); }
Verileri LocalStorage'dan almak için de bir fonksiyona ihtiyacımız olacak.
function restoreComment(): PostComment | undefined { const text = localStorage.getItem('draft'); return text ? JSON.parse(text) : undefined; }
Bu kodun nesi yanlış? İlk sorun, yorumu geri yüklerken, updatedAt
alanı için Date
yerine bir string
türü alacak olmamızdır.
Bunun nedeni, JSON'un diziler ve nesnelerin yanı sıra yalnızca dört temel veri türüne ( null
, string
, number
, boolean
) sahip olmasıdır. JSON'da bir Date
nesnesinin yanı sıra JavaScript'te bulunan diğer nesnelerin (fonksiyonlar, Harita, Küme vb.) kaydedilmesi mümkün değildir.
JSON.stringify
JSON biçiminde temsil edilemeyen bir değerle karşılaştığında tür dönüşümü gerçekleşir. Date
nesnesi durumunda, bir dize elde ederiz çünkü Date
nesnesi, Date
nesnesi yerine bir dize döndüren toJson() yöntemini uygular.
const date = new Date('August 19, 1975 23:15:30 UTC'); const jsonDate = date.toJSON(); console.log(jsonDate); // Expected output: "1975-08-19T23:15:30.000Z" const isEqual = date.toJSON() === JSON.stringify(date); console.log(isEqual); // Expected output: true
İkinci sorun, saveComment
işlevinin, tarih alanının Date
türünde olduğu PostComment
türünü döndürmesidir. Ancak Date
yerine string
tipini alacağımızı zaten biliyoruz. TypeScript bu hatayı bulmamıza yardımcı olabilir ama neden olmasın?
TypeScript'in standart kitaplığında JSON.parse
işlevinin (text: string) => any
olarak yazıldığı ortaya çıktı. any
kullanımından dolayı tür kontrolü esasen devre dışı bırakılır. Örneğimizde TypeScript, fonksiyonun Date
nesnesi içeren bir PostComment
döndüreceği yönündeki sözümüzü aldı.
Bu TypeScript davranışı uygunsuz ve güvensizdir. Bir dizeye Date
nesnesi gibi davranmaya çalışırsak uygulamamız çökebilir. Örneğin, comment.updatedAt.toLocaleDateString()
öğesini çağırırsak bozulabilir.
Aslında küçük örneğimizde, Date
nesnesini sayısal bir zaman damgasıyla değiştirebiliriz; bu, JSON serileştirmesinde işe yarar. Ancak gerçek uygulamalarda veri nesneleri kapsamlı olabilir, türler birden fazla konumda tanımlanabilir ve geliştirme sırasında bu tür bir hatayı belirlemek zorlu bir görev olabilir.
TypeScript'in JSON anlayışını geliştirebilseydik ne olurdu?
Başlangıç olarak, TypeScript'in hangi veri türlerinin güvenli bir şekilde JSON'a serileştirilebileceğini anlamasını nasıl sağlayacağımızı bulalım. safeJsonStringify
işlevini oluşturmak istediğimizi varsayalım; burada TypeScript, JSON'un serileştirilebilir olduğundan emin olmak için giriş veri biçimini kontrol edecektir.
function safeJsonStringify(data: JSONValue) { return JSON.stringify(data); }
Bu fonksiyonda en önemli kısım JSON formatında temsil edilebilecek tüm olası değerleri temsil eden JSONValue
tipidir. Uygulama oldukça basittir:
type JSONPrimitive = string | number | boolean | null | undefined; type JSONValue = JSONPrimitive | JSONValue[] | { [key: string]: JSONValue; };
Öncelikle tüm primitif JSON veri tiplerini açıklayan JSONPrimitive
tipini tanımlıyoruz. Ayrıca seri hale getirildiğinde undefined
değere sahip anahtarların atlanacağı gerçeğine dayanarak undefined
türü de dahil ediyoruz. Seri durumdan çıkarma sırasında bu anahtarlar nesnede görünmez; çoğu durumda bu da aynı şeydir.
Daha sonra JSONValue
tipini tanımlıyoruz. Bu tür, TypeScript'in kendilerine başvuran türler olan özyinelemeli türleri tanımlama yeteneğini kullanır. Burada JSONValue
bir JSONPrimitive
, bir JSONValue
dizisi veya tüm değerlerin JSONValue
türünde olduğu bir nesne olabilir. Sonuç olarak, JSONValue
tipindeki bir değişken, sınırsız iç içe yerleştirmeye sahip diziler ve nesneler içerebilir. Bunların içindeki değerlerin JSON formatıyla uyumluluğu da kontrol edilecektir.
Artık aşağıdaki örnekleri kullanarak safeJsonStringify
fonksiyonumuzu test edebiliriz:
// No errors safeJsonStringify({ updatedAt: Date.now() }); // Yields an error: // Argument of type '{ updatedAt: Date; }' is not assignable to parameter of type 'JSONValue'. // Types of property 'updatedAt' are incompatible. // Type 'Date' is not assignable to type 'JSONValue'. safeJsonStringify({ updatedAt: new Date(); });
Her şey düzgün çalışıyor gibi görünüyor. Fonksiyon tarihi sayı olarak iletmemize izin veriyor ancak Date
nesnesini iletirsek hata veriyor.
Ancak, işleve aktarılan verilerin bir değişkende saklandığı ve tanımlanmış bir türe sahip olduğu daha gerçekçi bir örneği ele alalım.
interface PostComment { authorId: string; text: string; updatedAt: number; }; const comment: PostComment = {...}; // Yields an error: // Argument of type 'PostComment' is not assignable to parameter of type 'JSONValue'. // Type 'PostComment' is not assignable to type '{ [key: string]: JSONValue; }'. // Index signature for type 'string' is missing in type 'PostComment'. safeJsonStringify(comment);
Artık işler biraz çetrefilleşiyor. TypeScript, JSONValue
türündeki bir işlev parametresine PostComment
türünde bir değişken atamamıza izin vermiyor çünkü "'PostComment' türünde 'string' türü için dizin imzası eksik".
Peki indeks imzası nedir ve neden eksik? JSON formatında serileştirilebilecek nesneleri nasıl tanımladığımızı hatırlıyor musunuz?
type JSONValue = { [key: string]: JSONValue; };
Bu durumda, [key: string]
dizin imzasıdır. Bu, "bu nesnenin, değerleri JSONValue
türüne sahip olan dize biçiminde herhangi bir anahtara sahip olabileceği" anlamına gelir. PostComment
türüne bir dizin imzası eklememiz gerektiği ortaya çıktı, değil mi?
interface PostComment { authorId: string; text: string; updatedAt: number; // Don't do this: [key: string]: JSONValue; };
Bunu yapmak, yorumun herhangi bir rastgele alan içerebileceği anlamına gelir; bu, bir uygulamada veri türlerini tanımlarken genellikle istenen sonuç değildir.
Dizin imzasıyla ilgili sorunun gerçek çözümü, tanımlanmış bir dizin imzası olmayan türler için bile alanlar üzerinde yinelemeli olarak yineleme yapılmasına olanak tanıyan Eşlenen Türler'den gelir. Jeneriklerle birleştirildiğinde bu özellik, herhangi bir T
veri türünün JSON biçimiyle uyumlu başka bir JSONCompatible<T>
türüne dönüştürülmesine olanak tanır.
type JSONCompatible<T> = unknown extends T ? never : { [P in keyof T]: T[P] extends JSONValue ? T[P] : T[P] extends NotAssignableToJson ? never : JSONCompatible<T[P]>; }; type NotAssignableToJson = | bigint | symbol | Function;
JSONCompatible<T>
türü, belirli bir T
türünün JSON'a güvenli bir şekilde serileştirilip serileştirilmeyeceğini denetleyen eşlenmiş bir türdür. Bunu, T
tipindeki her özelliği yineleyerek ve aşağıdakileri yaparak yapar:
T[P] extends JSONValue ? T[P] : ...
koşullu tür, özelliğin türünün JSONValue
türüyle uyumlu olup olmadığını doğrulayarak, güvenli bir şekilde JSON'a dönüştürülebilmesini sağlar. Bu durumda mülkün türü değişmeden kalır.T[P] extends NotAssignableToJson ? never : ...
koşullu tür, özelliğin türünün JSON'a atanabilir olup olmadığını doğrular. Bu durumda, özelliğin türü, özelliğin son türden etkili bir şekilde filtrelenmesi için never
biçimine dönüştürülür.
unknown extends T ? never :...
başlangıçta check unknown
türün, esasen any
türe eşdeğer olan boş bir nesne türü {}
ye dönüştürülmesini önlemek için kullanılır.
Bir başka ilginç yön de NotAssignableToJson
türüdür. İki TypeScript temel öğesinden (bigint ve sembol) ve olası herhangi bir işlevi açıklayan Function
türünden oluşur. Function
türü, JSON'a atanamayan değerlerin filtrelenmesinde çok önemlidir. Bunun nedeni, JavaScript'teki herhangi bir karmaşık nesnenin Object türünü temel alması ve prototip zincirinde en az bir işleve sahip olmasıdır (örneğin, toString()
). JSONCompatible
türü tüm bu işlevleri yineler; dolayısıyla işlevlerin kontrol edilmesi, JSON'a serileştirilemeyen herhangi bir şeyi filtrelemek için yeterlidir.
Şimdi bu türü serileştirme fonksiyonunda kullanalım:
function safeJsonStringify<T>(data: JSONCompatible<T>) { return JSON.stringify(data); }
Artık işlev genel bir T
parametresi kullanıyor ve JSONCompatible<T>
bağımsız değişkenini kabul ediyor. Bu, JSON uyumlu bir tür olması gereken T
türünde bir bağımsız değişken data
aldığı anlamına gelir. Artık fonksiyonu indeks imzası olmayan veri türleriyle kullanabiliriz.
İşlev artık JSONCompatible<T>
türünden uzanan genel bir T
parametresi kullanıyor. Bu, JSON uyumlu bir tür olması gereken T
türünde bir bağımsız değişken data
kabul ettiği anlamına gelir. Sonuç olarak fonksiyonu indeks imzası olmayan veri tipleriyle kullanabiliriz.
interface PostComment { authorId: string; text: string; updatedAt: number; } function saveComment(comment: PostComment) { const serializedComment = safeJsonStringify(comment); localStorage.setItem('draft', serializedComment); }
Bu yaklaşım, ağ üzerinden veri aktarımı, HTML'ye veri gömme, localStorage'da veri depolama, çalışanlar arasında veri aktarımı vb. gibi JSON serileştirmenin gerekli olduğu durumlarda kullanılabilir. Ayrıca, toJsonValue
yardımcısı, kesin olarak yazılmış bir nesne olmadan kullanılabilir. JSONValue
türündeki bir değişkene bir dizin imzasının atanması gerekir.
function toJsonValue<T>(value: JSONCompatible<T>): JSONValue { return value; } const comment: PostComment = {...}; const data: JSONValue = { comment: toJsonValue(comment) };
Bu örnekte toJsonValue
kullanmak, PostComment
türündeki eksik dizin imzasıyla ilgili hatayı atlamamızı sağlar.
Seri durumdan çıkarma söz konusu olduğunda, zorluk aynı anda hem daha basit hem de daha karmaşıktır çünkü alınan verinin formatına yönelik hem statik analiz kontrollerini hem de çalışma zamanı kontrollerini içerir.
TypeScript'in yazım sistemi açısından bakıldığında zorluk oldukça basittir. Aşağıdaki örneği ele alalım:
function safeJsonParse(text: string) { return JSON.parse(text) as unknown; } const data = JSON.parse(text); // ^? unknown
Bu örnekte any
dönüş türünü unknown
türle değiştiriyoruz. Neden unknown
seçmelisiniz? Temel olarak, bir JSON dizesi yalnızca almayı beklediğimiz verileri değil, her şeyi içerebilir. Örneğin, veri formatı farklı uygulama sürümleri arasında değişebilir veya uygulamanın başka bir kısmı aynı LocalStorage anahtarına veri yazabilir. Bu nedenle unknown
en güvenli ve en kesin seçimdir.
Ancak unknown
türle çalışmak, yalnızca istenen veri türünü belirtmekten daha az kullanışlıdır. Tip dökümünün dışında, unknown
tipi gerekli veri tipine dönüştürmenin birden fazla yolu vardır. Bu tür yöntemlerden biri, çalışma zamanında verileri doğrulamak ve veriler geçersizse ayrıntılı hatalar atmak için Superstruct kitaplığını kullanmaktır.
import { create, object, number, string } from 'superstruct'; const PostComment = object({ authorId: string(), text: string(), updatedAt: number(), }); // Note: we no longer need to manually specify the return type function restoreDraft() { const text = localStorage.getItem('draft'); return text ? create(JSON.parse(text), PostComment) : undefined; }
Burada, create
işlevi bir tür koruması görevi görerek türü istenen Comment
arayüzüne daraltır . Sonuç olarak, artık dönüş türünü manuel olarak belirtmemize gerek yok.
Güvenli bir seri durumdan çıkarma seçeneğinin uygulanması hikayenin yalnızca yarısıdır. Projedeki bir sonraki görevle uğraşırken onu kullanmayı unutmamak da aynı derecede önemlidir. Tüm anlaşmaların ve en iyi uygulamaların takip edilmesini sağlamak zor olabileceğinden, proje üzerinde büyük bir ekip çalışıyorsa bu özellikle zorlaşır.
TypeScript-eslint bu göreve yardımcı olabilir. Bu araç, güvenli olmayan any
kullanım örneklerinin belirlenmesine yardımcı olur. Özellikle JSON.parse
tüm kullanımları bulunabilir ve alınan verinin formatının kontrol edilmesi sağlanabilir. Kod tabanındaki any
türden kurtulma hakkında daha fazla bilgi TypeScript'i Gerçekten "Kesinlikle Yazılmış" Yapma makalesinde okunabilir.
Güvenli JSON serileştirme ve seri durumdan çıkarma işlemlerine yardımcı olmak için tasarlanmış son yardımcı program işlevleri ve türleri burada verilmiştir. Bunları hazırlanan TS Oyun Alanında test edebilirsiniz.
type JSONPrimitive = string | number | boolean | null | undefined; type JSONValue = JSONPrimitive | JSONValue[] | { [key: string]: JSONValue; }; type NotAssignableToJson = | bigint | symbol | Function; type JSONCompatible<T> = unknown extends T ? never : { [P in keyof T]: T[P] extends JSONValue ? T[P] : T[P] extends NotAssignableToJson ? never : JSONCompatible<T[P]>; }; function toJsonValue<T>(value: JSONCompatible<T>): JSONValue { return value; } function safeJsonStringify<T>(data: JSONCompatible<T>) { return JSON.stringify(data); } function safeJsonParse(text: string): unknown { return JSON.parse(text); }
Bunlar, JSON serileştirmesinin gerekli olduğu her durumda kullanılabilir.
Bu stratejiyi birkaç yıldır projelerimde kullanıyorum ve uygulama geliştirme sırasında olası hataları anında tespit ederek etkinliğini kanıtladı.
Bu makalenin size yeni bilgiler sunacağını umuyorum. Okuduğunuz için teşekkürler!