Bu genel TypeScript işlevini beklediğim gibi nasıl çalıştırabilirim?


TypeScript'te tür sistemiyle iyi çalışacak bir fonksiyon tanımlamaya çalışıyorum; böylece bir nesnenin anahtarını alabilir ve eğer o anahtarın değeri bazı değişikliklere ihtiyaç duyuyorsa (benim örneğimde özel bir string türünü boolean'a dönüştürmek gibi), bunu türleri dönüştürmeden yapabilirim.

İşte aynı adım adım anlatımı içeren ama derleyici hatalarını görmeyi kolaylaştıran bir TypeScript oynatma alanı bağlantısı.

Örneğime başlamak için bazı yardımcı türler:

type TTrueOrFalse = 'true' | 'false'
const toBool = (tf: TTrueOrFalse): boolean => tf === 'true'

İşlemek istediğimiz birkaç alanımız var. Bazıları sayıdır, bazıları ise TTrueOrFalse ile temsil ettiğimiz onay kutusu benzeri değerlerdir.

type TFormData = {
  yesOrNoQuestion: TTrueOrFalse
  freeformQuestion: number
}

// same keys as TFormData, but convert TTrueOrFalse to a bool instead, e.g. for a JSON API
type TSubmitFormToApi = {
  yesOrNoQuestion: boolean
  freeformQuestion: number
}

Bu, bir seferde bir form alanını işleyen fonksiyondur. Bu fonksiyon için TTrueOrFalse'u bir boolean'a dönüştürmemiz gerekiyor.

const submitFormField = <FieldName extends keyof TFormData>(
    fieldName: FieldName,
    value: TSubmitFormToApi[FieldName]
) => { /* some code here */}

İşte sorun şu. Bu fonksiyon, bir form alanını ve değerini almalı ve önce TTrueOrFalse değerlerini booleanlara dönüştürerek API'ye göndermelidir.

const handleSubmit = <
    FieldName extends keyof TFormData
  >(
    fieldName: FieldName,
    value: TFormData[FieldName]
) => {
  // I want to convert `TTrueOrFalse` to a `bool` for my API, so I check if we are dealing with that field or not.
  // seems like this check should convince the compiler that the generic type `FieldName` is now `'yesOrNoQuestion'` and
  // that `value` must be `TFormData['yesOrNoQuestion']`, which is `TTrueOrFalse`.
  if (fieldName === 'yesOrNoQuestion') {

    // `value` should be interpreted as type `TTrueOrFalse` since we've confirmed `fieldName === 'yesOrNoQuestion'`, but it isn't
    submitFormField(
      fieldName,
      toBool(value) // type error
    )

    // Looks like the compiler doesn't believe `FieldName` has been narrowed down to `'yesOrNoQuestion'`
    // since even this cast doesn't work:
    submitFormField(
      fieldName,
      toBool(value as TTrueOrFalse) // type error
    )

    // so I'm forced to do this, which "works":
    submitFormField(
      fieldName as 'yesOrNoQuestion',
      toBool(value as TTrueOrFalse)
    )
  }

  // so I thought maybe I can use a manual type checking function, but it seems like
  // the fact that `FieldName` is a union of possible strings is somehow making what I want
  // to do here difficult?
  const isYesOrNo = (fn: FieldName): fn is 'yesOrNoQuestion' => fieldName === 'yesOrNoQuestion'

  // not referencing the generic type from the function, FieldName, works here though:
  const isYesOrNoV2 = (fn: Extract<keyof TFormData, string>): fn is 'yesOrNoQuestion' => fieldName === 'yesOrNoQuestion'

  // ok, so let's try again.
  if (isYesOrNoV2(fieldName)) {
    // seems like now the compiler believes FieldName is narrowed, but that doesn't narrow
    // the subsequent type I defined for `value`: `TFormData[FieldName]`
    submitFormField(
      fieldName,
      toBool(value) // type error
    )

    // At least this seems to work now, but it still sucks:
    submitFormField(
      fieldName,
      toBool(value as TTrueOrFalse)
    )
  }
}

Şunu unutmayın, handleSubmit iç kısmında yapmak istediklerimle ilgili sorunlar olsa da, derleyici en azından çağırma açısından ne yapmak istediğimi anlıyor:

handleSubmit('yesOrNoQuestion', 'false')
handleSubmit('yesOrNoQuestion', 'true')
handleSubmit('yesOrNoQuestion', 'should error') // fails as expected

handleSubmit('freeformQuestion', 'not a number') // fails as expected
handleSubmit('freeformQuestion', 32) 

handleSubmit('errorQuestion', 'should error') // fails as expected
handleSubmit('errorQuestion', 12) // fails as expected

Bütün bunlar boyunca, sorunun bir kısmının, handleSubmit içine fieldName için geçirdiğim bir şeyin hâlâ şu birleşim türünde olabileceğini varsayıyorum: 'yesOrNoQuestion' | 'freeformQuestion', şöyle:

// (simulate not knowing the fieldName at compile time)
const unknownFieldName: Extract<keyof TFormData, string> = new Date().getDay() % 2 === 0 ? 'yesOrNoQuestion' : 'freeformQuestion'

// now these compile, problematically, because the expected value is of type `'true' | 'false' | number`
// but I don't want this to be possible.
handleSubmit(unknownFieldName, 2)

İdeal olarak, handleSubmit'i dinamik olarak çağırabileceğim tek yol, bir TFormData türünde nesne üzerinde gezinmek ve her bir anahtar/değer çifti ile handleSubmit'i çağırmaktır; bu çift, derleyici tarafından doğru türde olduğu bilinecektir.

handleSubmit için gerçekten tanımlamak istediğim, tam olarak TFormData’nın bir anahtarını ve anahtarın karşılık gelen değer türünde bir değeri alan bir fonksiyon. fieldName için birleşim türü almasına izin verilen bir şeyi tanımlamak istemiyorum ama bunun mümkün olup olmadığını bilmiyorum.

Belki fonksiyon aşırı yüklemesinin yardımcı olabileceğini düşündüm, ama bunu daha uzun bir form türü için tanımlamak zahmetli olurdu:

function handleSubmitOverload(fieldName: 'yesOrNoQuestion', value: TTrueOrFalse): void
function handleSubmitOverload(fieldName: 'freeformQuestion', value: number): void
function handleSubmitOverload<FieldName extends keyof TFormData>(fieldName: FieldName, value: TFormData[FieldName]): void {
  if (fieldName === 'yesOrNoQuestion') {

    // This still doesn't work, same problem inside the overloaded function since the 
    // concrete implementation's parameter types have to be the same as the non-overloaded try above
    submitFormField(
      fieldName,
      toBool(value) // type error
    )
  }
}

// still works from the outside:
handleSubmitOverload('yesOrNoQuestion', 'false')
handleSubmitOverload('yesOrNoQuestion', 'wont work') // fails as expected

// At least the overloaded version does handle this other problem with our first attempt,
// since it no longer accepts the union of value types when the field name's type is not specific enough
handleSubmitOverload(unknownFieldName, 'false') // error, no matching overload
handleSubmitOverload(unknownFieldName, 42) // error, no matching overload

handleSubmit işlevini, hem işlev içinde hem dışında, tür güvenliğini sağlayacak şekilde ve tip dönüştürme yapmadan tanımlamanın bir yolu var mı?

Düzeltme: Bence bunun işe yarayacağını bildiğimi belirtmekte fayda var:

const handleSubmitForWholeForm = (
  formField: keyof TFormData,
  form: TFormData
) => {
  if (formField === 'yesOrNoQuestion') {
    submitFormField(formField, toBool(form[formField]))
  }
}

Ama bu, bu soruyu dayandırdığım gerçek kodun yapılandırılması şekli değil.

Cevap #1

TypeScript henüz kontrol akışı analizi yoluyla tip parametrelerini daraltmayı bilmiyor (bkz. microsoft/TypeScript#24085). Bu, eğer handleSubmit() fonksiyonunuzu alan adı tipi N üzerinde genel yaparsanız, fieldName değerini kontrol etmenin Nyi daraltmayacağı ve dolayısıyla TFormData[N] tipinin de daraltılmayacağı anlamına gelir.

İlerlemenin olası bir yolu, fonksiyonu somut yapmak ve generik olmamasıdır. Ama fieldName ve value parametrelerini nasıl ilişkilendirebiliriz? Rest parametre demetlerini kullanabiliriz. Özellikle, eğer AllParams adlı bir türü şöyle tanımlarsak:

type AllParams = { [N in keyof TFormData]: [N, TFormData[N]] }[keyof TFormData]
// type AllParams = ["yesOrNoQuestion", TTrueOrFalse] | ["freeformQuestion", number]

o zaman handleSubmit imzasını (...nv: AllParams) => void gibi bir şey yapabiliriz. AllParams, tüm kabul edilebilir fieldName ve value çiftlerinin bir birleşimidir (ve yukarıdaki tanım daha uzun formlar için ölçeklenebilir olmalıdır).

İşte handleSubmit() uygulaması:

const handleSubmit = (...nv: AllParams) => {
  if (nv[0] === "yesOrNoQuestion") {
    submitFormField(nv[0], toBool(nv[1]));
  } else {
    submitFormField(nv[0], nv[1]);
  }
}

nv öğesini ayrı fieldName ve value değişkenlerine ayırarak çözemiyorsunuz, yoksa aralarındaki ilişki kaybolur. Bunun yerine nv[0] ve nv[1] öğelerini kullanmalı ve yukarıda gösterildiği gibi nv[0] testine dayalı olarak nv'yi daraltmak için kontrol akışı analizine güvenmelisiniz.

Bu fonksiyon, yalnızca doğru türde parametre çiftlerini kabul edeceği ve alan adlarının birleşimlerini kabul etmeyeceği noktada, sizin aşırı yüklenmiş fonksiyonunuz gibi çalışmalıdır:

handleSubmit('yesOrNoQuestion', 'false') // okay
handleSubmit('yesOrNoQuestion', 'wont work') // error
handleSubmit('freeformQuestion', 3); // okay
handleSubmit(Math.random() < 0.5 ? 'yesOrNoQuestion' : 'freeformQuestion', 1); // error

Bunu söyledikten sonra, genellikle fonksiyonlara geçirilen korele edilmiş türlerle başa çıkma yöntemim, orijinal handleSubmit() uygulamanızda kendi başınıza yaptığınız gibi bazı dikkatli tür dönüşümleri kullanmaktır. Eğer rest parametresi olmayan bir fonksiyon imzasının pratikliğini tercih ediyorsanız, sadece toBool(value as any as TTrueOrFlase) kullanabilir ve devam edebilirsiniz.


Tamam, umarım yardımcı olur; iyi şanslar!

Koda bağlantı

İlgili makaleler