TypeScript generik tipi ile kesişim ve Partial kullanımı


Generics ve partial ile ilgili oldukça karmaşık bir TypeScript sorunum var.

Bu konu için bir oyun alanı burada

Sorunu React terimleri kullanarak açıklayacağım, ancak bunun React ile doğrudan hiçbir ilgisi yok.

Temel olarak bir useReducer kullanarak bir reducer oluşturan ve bazı yaygın özelliklerin yanı sıra genel bir data özelliği içeren özel bir React hook'u var.

Hadi bunu kanca olarak farz edelim:

interface IState<T> {
    some: string;
    data: T;
}

type Action<T> = { type: 'UPDATE_STATE'; payload: Partial<T> };

const foo = <T>(initalState: T) => {
    const state: IState<T> = { some: 'foo', data: initalState };
    const dispatch = (action: Action<T>) => {
        // do something
    }

    return {state, dispatch};
};

Ayrıca, aslında ilkini genişleterek durum indirgesine ek ortak özellikler ekleyen başka bir özel kanca daha vardır:

interface MyBar {
    extraData: string;
}

const bar = <T>(initialState: T) => {
    const {state, dispatch} = foo<T & MyBar>({ ...initialState, extraData: 'some data' });
    
    return { state, dispatch };
}

Şimdi, tipik bir kullanım şöyle olur:

const baz = () => {
    const { state, dispatch } = bar<IMyState>({ name: 'mor' });
    console.log(state, dispatch)
}

Yani baz içinde, durum nesnem şöyle olacak:

{
 "extraData": "some data",
 "some": "foo",
 "data": {
  "name": "mor"
 }
}

Şimdiye kadar her şey iyi. Şimdi aşağıdaki değişikliği yapmak istiyorum:

const bar = <T>(initialState: T) => {
    const {state, dispatch} = foo<T & MyBar>({ ...initialState, extraData: 'some data' });
    
    **dispatch({ type: 'UPDATE_STATE', payload: { extraData: 'changed' } });**
    return { state, dispatch };
}

Ve alıyorum:

Type '{ extraData: "changed"; }' is not assignable to type 'Partial<T & MyBar>'.(2322)
input.ts(7, 42): The expected type comes from property 'payload' which is declared here on type 'Action<T & MyBar>'

Bunun neden olduğunu tam olarak bilmiyorum, çünkü bar ekstra veri ile birlikte generik tipi de geçiriyor, foo'nun bunu izin verebileceğini beklerdim.

Her türlü tavsiyeyi memnuniyetle karşılarım!

Cevap #1

TypeScript, bu durumda atama yapılabilirlik hakkında herhangi bir varsayımda bulunamıyor çünkü T herhangi bir türe tahmin edilmiyor. Bu, bir Pandora kutusu gibi.

Bu örneği düşünün:

interface IState<T> {
  some: string;
  data: T;
}

type Action<T> = { type: 'UPDATE_STATE'; payload: Partial<T> };

const foo = <T,>(initalState: T) => {
  const state = { some: 'foo', data: initalState };
  const dispatch = (action: Action<T>) => { }

  return { state, dispatch };
};

interface MyBar {
  extraData: string;
}

const bar = <T extends Record<string, unknown>,>(initialState: T) => {
  const { state, dispatch } = foo({...initialState, extraData: 'some data' });
  let x: Partial<T>
  let y: { extraData: string; }

  x = y // error
  y = x // error


  // TS is unable to make any assumptions about T
  dispatch({ type: 'UPDATE_STATE', payload: { extraData: 's' } })
  return { state, dispatch };
}

const baz = () => {
  const { state, dispatch } = bar({ name: 'mor', extraData: 's' });
  dispatch({ type: 'UPDATE_STATE', payload: { extraData: 's' } })
  console.log(state, dispatch)
}

Oyun alanı

Bahsettiğim kodun bu bölümü:

  let x: Partial<T>
  let y: { extraData: string; }

  x = y // error
  y = x // error

x ve y birbirine atanmıyor. Peki ya T bu nesne { name: string } ise - o zaman bu davranış güvensizdir. Çünkü bu nesnelerin ortak hiçbir şeyi yok.

Biliyorum, sen bar çağırdığında TS'nin tipi çıkarabildiğini ama unutma, biz çağırma aşamasında değiliz..

Ayrıca, asıl sorun statik extraData özelliğindedir. Bu da genel parametreyle parametrize edilmelidir.

Bu örneği düşünün:

interface IState<T> {
  some: string;
  data: T;
}

type Action<T> = { type: 'UPDATE_STATE'; payload: Partial<T> };

const foo = <T,>(initalState: T) => {
  const state = { some: 'foo', data: initalState };
  const dispatch = (action: Action<T>) => { }
  return { state, dispatch };
};

interface MyBar {
  extraData: string;
}

const bar = <T, U>(initialState: T, extraData: U) => {
  const { state, dispatch } = foo({ ...initialState, ...extraData });

  dispatch({ type: 'UPDATE_STATE', payload: { ...extraData } })
  return { state, dispatch };
}

const baz = () => {
  const { state, dispatch } = bar({ name: 'mor' }, { extraData: 's' });
  dispatch({ type: 'UPDATE_STATE', payload: { extraData: 's' } })
  console.log(state, dispatch)
}

Oyun alanı

Cevap #2

Aşağıdaki koda bakın:

const {state, dispatch} = foo<T & MyBar>({ ...initialState, extraData: 'some data' });
dispatch({ type: 'UPDATE_STATE', payload: { extraData: 'changed' } });

Sen dispatch'e Action<T & MyBar> göndermek zorundasın ama sadece MyBar türünü gönderdin! Yani iki seçeneğin var:

Sadece extraData: 'changed' parametresi göndermek istiyorsanız & yerine | kullanın veya aşağıdaki gibi ...initialState parametresini de ekleyin:

dispatch({ type: 'UPDATE_STATE', payload: { ...initialState, extraData: 'changed' } }); 

PlaygroundLink

İlgili makaleler