2

I'm trying to write a React component where its properties change based on the type of a generic property.

interface PropsBase<T> {
  value: T;
  children: (value: T) => ReactElement;
}

type ArrayType<T extends any[]> = T extends Array<infer U> ? U : never;

interface ArrayProps<T extends any[]> extends Omit<PropsBase<T>, 'children'> {
  children: (value: ArrayType<T>, index: number, array: T) => ReactElement;
}

type Props<T> = T extends any[] ? ArrayProps<T> : PropsBase<T>;

const MyComp = <T extends any>({ value, children }: Props<T>) => {
  if (Array.isArray(value)) {
    return <>{value.map(children)}</>;
  }
  
  return <div>{children(value)}</div>;
};

If value is an array, then the child it expects is a function that's applied to each element in the array. Otherwise it expects a function that just takes in the value.

For reasons I have yet to determine, when Props is conditional as it is above, it cannot determine the type of T.

const val = { a: 10 };
const rendered = 
  <MyComp value={val}>
    {value => <span>{value.a}</span>}
  </MyComp>;

// error: Object is of type 'unknown'.

This is the case even if both results of the ternary operator are identical.

type Props<T> = T extends any[] ? PropsBase<T> : PropsBase<T>;
// same error

Any other props, including ones only listed in ArrayProps are correctly inferred.

interface ArrayProps<T extends any[]> extends Omit<PropsBase<T>, 'children'> {
  children: (value: ArrayType<T>, index: number, array: T) => ReactElement;
  arrayOpt: boolean;
}

const array = [ 1, 2, 3 ];
const renderedArray =
  <MyComp value={array} arrayOpt>
    {value => <span>{value}</span>}
  </MyComp>;

If I had to guess, this is a bug in tsc. Meanwhile, is there any way around this?

TS Playground

1 Answer 1

1

It's messy, but I found a workaround.

interface MyComp<T> extends FunctionComponent<Props<T>> {};
function MyComp<T extends any[]>(props: ArrayProps<T>): ReactElement;
function MyComp<T>(props: PropsBase<T>): ReactElement;
function MyComp<T>(props: Props<T>) {
  const {
    value,
    children,
    arrayOpt,
  } = props as PropsBase<T> & { arrayOpt: boolean };

  if (Array.isArray(value)) {
    return <>{value.map(children)}</>;
  } else {
    return <div>{(children as PropsBase<T>['children'])(value)}</div>;
  }
};

TS Playground

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.