TypeScript veri sınıflarında tekrar/boilerplate nasıl azaltılır?


TypeScript öğreniyorum (Kotlin/Java, Dart/Flutter, Python ve çeşitli Lisp dillerinde deneyimli bir programcıyım). TS veri sınıfları için gerekli gibi görünen çok miktarda boilerplate kod beni şaşırttı. İşte basitleştirilmiş bir örnek: Basit bir Student(id, firstName, middleName, lastName, enrolledOn) sınıfına ihtiyacım var. Ayrıca şunları da istiyorum:

  • yapıcı için adlandırılmış parametreler (çağrı noktalarını okunabilir kılmak için)
  • ve bir copyWith yöntemi.

İşte ihtiyacım gibi görünen şey:

class Student {
  constructor({
    id,
    firstName,
    middleName,
    lastName,
    enrolledOn,
  }: {
    id: string;
    firstName: string;
    middleName: string;
    lastName: string;
    enrolledOn?: Date;
  }) {
    this.id = id;
    this.firstName = firstName;
    this.middleName = middleName;
    this.lastName = lastName;
    this.enrolledOn = enrolledOn;
  }

  copyWith({
    id = this.id,
    firstName = this.firstName,
    middleName = this.middleName,
    lastName = this.lastName,
    enrolledOn = this.enrolledOn,
  }: {
    id?: string;
    firstName?: string;
    middleName?: string;
    lastName?: string;
    enrolledOn?: Date;
  }): Student {
    return new Student({
      id,
      firstName,
      middleName,
      lastName,
      enrolledOn,
    });
  }
}

Aynı alanları tekrar etmem gereken sayı 6 kez! Gerçek durumumda 9-10 alanı olan bazı veri kayıtlarım var (ve birçok isteğe bağlı alan), bu yüzden bu gerçekten çok fazla şablon kodu.

Önceki işimde, Java'da Immutables, Kotlin'de yerleşik veri sınıflarını ve Dart'ta Freezed'i çok fazla tekrarlayan kodu azaltmak için kullandım.

Örneğin, Freezed kullanarak Dart'taki yukarıdaki sınıf şöyle olurdu:

@freezed
class Student with _$Student {
  const factory Student({
    required String id,
    required String firstName,
    required String middleName,
    required String lastName,
    DateTime? enrolledOn,
  }) = _Student;
}

Tabii ki ChatGPT vb. araçlara sordum ve immer ile immutable.js ile karşılaştım. Ama onları kullansam bile, isimlendirilmiş yapıcı parametrelerine ve copyWith yöntemlerine sahip olmak için, TS'de gereksiz yere çok fazla tekrar kod gibi görünüyor mu?

Veri sınıflarını yönetmeyi kolaylaştırabilecek kütüphaneler veya teknikler var mı? Yoksa bunu Typescript/JS öncelikli bir yaklaşımla mı düşünmüyorum?

Cevap #1

Bunu yapmanın birçok yolu vardır. Muhtemelen en basit ve en az tartışmalı olanı, satır içi türlerinizi bir arayüze çıkarmaktır.

Daha tartışmalı olan, kısayol olarak Object.assign() kullanmak olacaktır. TypeScript, Object.assign konusunda çok sınırlı tür denetimi yeteneklerine sahiptir (GitHub issue #26792'ye bakınız), ancak kodunuzu iyi biliyorsanız ve bununla rahat hissediyorsanız, bu kodunuzun boyutunu önemli ölçüde azaltabilir.

interface StudentConfig {
  id: string;
  firstName: string;
  middleName: string;
  lastName: string;
  enrolledOn?: Date;
}

class Student implements StudentConfig {
  id!: string; // The "!" tells TypeScript that this property is set
               // even though the compiler can't tell how
  firstName!: string;
  middleName!: string;
  lastName!: string;
  enrolledOn?: Date;

  constructor(config:StudentConfig) {
    Object.assign(this, config);
  }

  // Partial<StudentConfig> makes all of the properties on
  // StudentConfig optional
  copyWith(config: Partial<StudentConfig>): Student {
    return new Student(Object.assign({}, this, config));
  }
}


// ============

const foo = new Student({
    id: "foo",
    firstName: "Foo",
    lastName: "Foo",
    middleName: "Foo",
});
const bar = foo.copyWith({
    id: "bar",
    firstName: "Bar",
}); // { id: "bar", firstName: "Bar", lastName: "Foo", middleName: "Foo" } 

Sonuçta, çoğu şey ihtiyaçlarınıza bağlı. Size makul derecede kısa bir cevap veriyorum çünkü bunu istediniz, ama bazı ödünler var. Örneğin, Object.assign() çok verimli değil. Ve ! operatörü TypeScript’in tür kontrolü yeteneğinin bazılarını geçersiz kılıyor (ancak Student’in StudentConfig’i uygulaması, bu riskin çoğunu azaltmaya yardımcı oluyor). Yani ödünleri kendiniz tartmanız gerekecek.

Cevap #2

TypeScript, JavaScript'in üzerine eklenen statik tiplere çok benzer, bu yüzden TypeScript'in sınırlamalarını her zaman JavaScript'in sınırlamaları açısından düşünmek en iyisidir.

Diğer dillerdeki deyimler her zaman bire bir çevrilmez. Kodunuzu daha deyimsel hale getirerek kalıp metinden nasıl kurtulabileceğimizi keşfedelim.

JavaScript ve TypeScript veri nesneleri veya DTO'lar (Data Transfer Objects), genellikle sınıf örnekleri yerine sıradan nesneler olarak kullanılır. Bunun nedeni, JSON'a ve JSON'dan serileştirmeyi basitleştirmesidir. Bir sınıf örneğine deserialize edemezsiniz. Bu, diğer dillerde struct kullanmaya benzer.

Sınıflar yerine sade eski nesneleri kullanmamızı önereceğim. Student bir arayüz haline gelir ve copyWith(..) yöntemi createStudentWith(..) fonksiyonu olur.

interface Student {
  id: string;
  firstName: string;
  middleName: string;
  lastName: string;
  enrolledOn?: Date;
}

function createStudentWith(student: Student, partial: Partial<Student>) {
  return { ...student, ...partial };
}

const studentA: Student = { id: 'abc', firstName: 'Joe', middleName: 'Blogs', lastName: 'Blow' };
const studentB = createStudentWith(studentA, {id: 'zyx', firstName: 'jane'}); 

console.log(studentA, studentB);

Çalışan bir örneği TypeScript Playground'da görebilirsiniz: https://www.typescriptlang.org/play?ssl=16&ssc=33&pln=1&pc=1#code/JYOwLgpgTgZghgYwgAgMpgK4BMLmQbwChllgsAuZAZzClAHMBuY5GYKGgOTgFsJKadEExY8yWADYRufAbQbMSEuF179q84YuS4oAewlSsAeRAB+SgBE4kZgF9ChGBhAIwwPSGQIoEGxHRsXDAAdWAwAAsAChog8EpAnHAAGmQABzgodzgJSgAFTOyJAB5E4IA+AEoCFl9MKC98ZAA6VtiksFTW5oys4BzkO3tHBE8aDTiwAEEEzA7kAF4CUgpkAHI4ACMENdS2DjAZdTWAKT0IXeQxLElpNUo1gCEJPXoqS+VVWXXnvQB3NaDZijEDjdrBR6Lby+fxlcBhSIxObBKapfBkB4ALwAngAPS77L7HABWcBAFzslUYyBGYwMEGaL3oSMmqImHUeVMIQA