0

I have a function with an object as an input that is generic, I want to provide a default implementation by making the generic T a string and provide the needed arguments with default values that uses a string instead

type Props<T> = {
  items: T[];
  renderItem: (item: T) => JSX.Element;
};

const defaultRenderItem = (item: string) => item;
const defaultItems = ['1', '2'];

function render<T>({ items = defaultItems, renderItem = defaultRenderItem }: Props<T>) {
  return items.map(renderItem);
}

you can check a running example of this code over here

the problem is that typescript is complaining that 'T' could be instantiated with an arbitrary type which could be unrelated to 'string'.ts(2322)

I do understand typescript's concern, can somebody point me or provide an alternative on how I can achieve the same result without the error

2
  • Can I assume you want to be able to call render({})? Or do you need to write render({items: undefined, renderItem: undefined}) if you want the defaults? Commented Jun 28, 2020 at 20:06
  • @jcalz yes you can assume that I want to be able to call render({}) Commented Jun 28, 2020 at 23:43

1 Answer 1

1

It's possible to assert your way out of the error in the implementation side of the function, but I presume you want to prevent someone from calling things like:

render({ renderItem: (x: number) => x - 2 });

This would cause a runtime error, but the compiler would happily infer number for T and pretend everything is fine.


To fix that, I think maybe we should give up on using default values in the call signature itself and use overloads to represent the two distinct call cases: if you pass an incomplete Props, then it must be a Props<string>. Otherwise, if you pass a complete Props, you can use any generic type you'd like as long as it matches. We'll deal with default values inside the implementation. Which might look like this:

function render(stringProps: Partial<Props<string>>): string[];
function render<T>(otherProps: Props<T>): T[];
function render(props: Partial<Props<any>>) {
    const items = props.items || defaultItems;
    const renderItem = props.renderItem || defaultRenderItem;
    return items.map(renderItem);
}

Using Partial<Props<any>> inside the implementation is similar to using a type assertion, in that the compiler will not complain about props.items || defaultItems not being a T[], since it will definitely be an any[]. Implementations of overloads are not very strongly checked, so we have to be careful.

Anyway, then you'd use it like this, presumably as desired:

// first overload, Partial<Props<string>> 
const a = render({}); // string[]
const b = render({ renderItem: x => x.toUpperCase() }); // okay, x must be string
// second overload, Props<T> for generic <T>
const c = render({ items: [1, 2, 3], renderItem: x => x - 2 }); // okay, d is number[]

// errors
const d = render<number>({}); // error! {} is not a Props<number>
const e = render({ renderItem: (x: number) => x - 2 }); // error! if you leave out a property, it must be string-typed 
const f = render({ items: [1, 2, 3], renderItem: (x: string) => x.toUpperCase() }); // error! mismatch types

The compiler will now yell at you if you try to call render() with something incomplete unless that incomplete thing is consistent with choosing string as T in the generic version. Hopefully this gives you enough to work with. Good luck!

Playground link to code

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.