How to avoid undefined error in Typescript when extending?

37 Views Asked by At

My Typescript file:

interface BaseDefaultData{
  names: string[];
}

interface ExtendedDefaultData extends BaseDefaultData{
  defaultItems: {results: string[]; function: (props: string) => void;};
}

interface BaseProps {
  items: number;
  defaultData: BaseDefaultData;
}

interface ExtendedProps extends Omit<BaseProps, 'defaultData'>{
  defaultData: ExtendedDefaultData;
}

type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any
  ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>>
  : never;

type StrictUnion<T> = StrictUnionHelper<T, T>;

type Props = StrictUnion<BaseProps | ExtendedProps>;

I have strictNullChecks set to true. I'm having some issues in areas like:

defaultData.defaultItems?.results ?? [];

if(defaultData.defaultItems){
  defaultData.defaultItems.function(doSomething);
};

Typescript keeps complaining about Property 'defaultItems' does not exist on type 'BaseProps' every single place where I have defaultData.defaultItems. Yes, on one side it is the case, but for that I have checks using ternary operator in one place and if prop exist in another, so ideally I would expect not to have errors everywhere, because I check for prop existence. I can avoid error by having defaultData['defaultItems'] written instead, but it feels like I'm tricking Typescript. I expected checks that I have sufficient and don't understand why they don't work? What would be a proper way of handling it?

Component can be called by either:

<Component items={3} defaultData={{names: ['white', 'blue', 'red']}}/>

Or using

import {setItems} from '../../../redux/actions/itemActions';

<Component items={3} defaultData={{names: ['white', 'blue', 'red'], defaultItems: {results: ['1324', '6754232', '676729'], function: setItems}}}/>
1

There are 1 best solutions below

0
cefn On

Jcalz was asking you for a repro like this playground where accessing defaultItems is an error even though the code apparently checks for it...

interface BaseDefaultData{
  names: string[];
}

interface ExtendedDefaultData extends BaseDefaultData{
  defaultItems: {results: string[]; function: (props: string) => void;};
}

interface BaseProps {
  items: number;
  defaultData: BaseDefaultData;
}

interface ExtendedProps extends Omit<BaseProps, 'defaultData'>{
  defaultData: ExtendedDefaultData;
}

function Component({defaultData}: BaseProps | ExtendedProps){
  if(defaultData.defaultItems){
    defaultData.defaultItems?.results ?? [];
    defaultData.defaultItems.function("doSomething");
  }
}

However the check you are attempting is itself not valid code, since the defaultItems member is not present for all members of the union, so you can't access it and check its truthiness.

However, you can ask if a key exists on an object (whether it exists or not). So if you use the guard if("defaultItems" in defaultData){ instead, this can differentiate between union types and settle on the Extended ones...

interface BaseDefaultData{
  names: string[];
}

interface ExtendedDefaultData extends BaseDefaultData{
  defaultItems: {results: string[]; function: (props: string) => void;};
}

interface BaseProps {
  items: number;
  defaultData: BaseDefaultData;
}

interface ExtendedProps extends Omit<BaseProps, 'defaultData'>{
  defaultData: ExtendedDefaultData;
}

function Component({defaultData}: BaseProps | ExtendedProps){
  if("defaultItems" in defaultData){
    defaultData.defaultItems?.results ?? [];
    defaultData.defaultItems.function("doSomething");
  }
}

See this typescript playground for the solution.