ZiiMakc
ZiiMakc

Reputation: 36826

Typescript type fails if object is destructed

I have a function that return object with two props (res, mes) where one of them is null:

const fetchJSON = <Res, Body>(link: string, body: Body): Promise<{ res: Res; mes: null } | { res: null; mes: Popup }> => {
  return new Promise(resolve => {
    fetch(link,
      body: JSON.stringify(body)
    })
      .then(res => res.json())
      .then((resJson: Res) => {
        resolve({ res: resJson, mes: null })
      })
      .catch(err => {
        resolve({ res: null, mes: { type: 'err', text: 'some error' } })
      })
  })
}

If after i use response of fetch without desctruction everything works fine:

 const result = await fetchJSON<ResReaderData, ApiReaderData>('api/reader_data', { id })
 if (result.mes) return popupPush(result.mes)
 setProfile(result.res.reader)

But if i use object descruction:

const { res, mes } = await fetchJSON<ResReaderData, ApiReaderData>('api/reader_data', { readerId, role: 'alfa' })
  if (mes) return popupPush(mes)

  console.log(res.id)

Typescript doesn't understand that res is not null even if i checked mes:

enter image description here

Is there a way to fix this or i just need to forget about object destruction?

Or maybe there another approach for wrappers like this?

Upvotes: 3

Views: 789

Answers (2)

Soc
Soc

Reputation: 7780

If you know that the property that you are dealing with won't be null. You can use the non-null assertion operator (!) to assert that it isn't null or undefined.

Example console.log(res!.id):

function getResult(type: 'res' | 'mes') {
  let result: {
    res?: { foo: string },
    mes?: { bar: string },
  } = {};

  if (type === 'res') {
    result.res = {
      foo: 'res',
    };
  } else {
    result.mes = {
      bar: 'mes',
    };
  }

  return result;
}

const { res, mes } = getResult('res');

console.log(res!.foo); // non-null assertion
console.log(mes?.bar); // optional chaining

Additionally, you can use the optional chaining operator (?) to have the expression return null if the preceding property is null.

Upvotes: 1

Tadhg McDonald-Jensen
Tadhg McDonald-Jensen

Reputation: 21453

as far as I know this is a current limitation of typescript. at the time you deconstruct the type is { res: Res; mes: null } | { res: null; mes: Popup } so when unpacking it has to consider all possibilities meaning both res and messages can be null.

After seperating the 2 variables there is no way for the union before to link the 2 variables, they are separate. I look forward to a time when this limitation can be overcome but as of now you are stuck with keeping guards within one object.

one possible solution if your data structures allow it is to just return one field that can be differentiated:

const fetchJSON = <Res, Body>(link: string, body: Body): Promise<{ res: Res; mes: null } | { res: null; mes: Popup }> => {
  return new Promise(resolve => {
    fetch(link,
      body: JSON.stringify(body)
    })
      .then(res => res.json())
      .then((resJson: Res) => {
        resolve(resJson)
      })
      .catch(err => {
        resolve({ type: 'err', text: 'some error' })
      })
  })
}

function isErrorMessage(obj: any): obj is Popup {
    return obj && obj.type === "err";
}

async function MAIN(){

    const result = await fetchJSON<ResReaderData, ApiReaderData>('api/reader_data', { id })
     if (isErrorMessage(result)) return popupPush(result)
     setProfile(result.reader)
}

Upvotes: 1

Related Questions