I don't think TypeScript has anything built in for this, not least because "memoized" is a relative term.
I would make ShowData responsible for getting the data, and have it give DataRenderer the actual data, not a callback to request it. Simpler, and it bypasses the problem with the infinite render loop.
If you don't want to do that and you want a type-based solution, you could brand functions you consider memoized. Here's the Memoized type and a hook to do the memoization:
type Memoized<T> = T & {__memoized__: true};
const useMemoized = <FunctionType extends () => unknown,>(fn: FunctionType, deps: DependencyList | undefined) => {
const x = useMemo(fn, deps) as Memoized<FunctionType>;
x.__memoized__ = true;
return x;
};
Note that this doesn't ensure proper memoization (because that's context-sensitive), purely that the function has been asserted to be memoized.
Here's what DataRenderer would look like (with a couple of typos fixed as well):
const DataRenderer = (props: {asyncDataFn: Memoized<() => Promise<object[]>>}) => {
const [data, setData] = useState<object[]>([]);
useEffect(() => {
props.asyncDataFn().then(r => setData(r));
}, [props.asyncDataFn]);
// ...
};
Now this works:
const ShowData = () => {
// Works
const asyncDataFn = useMemoized(async () => { return [{/*...*/}]; }, []);
return <DataRenderer asyncDataFn={asyncDataFn}/>
};
But this raises an error as desired:
const ShowData = () => {
// Error as requested
const asyncDataFn = async () => { return [{/*...*/}]; };
return <DataRenderer asyncDataFn={asyncDataFn}/>
// ^^^^^^^^^^^ Type '() => Promise<{}[]>' is not assignable
// to type 'Memoized<() => Promise<object[]>>'.
// Property '__memoized__' is missing in type
// '() => Promise<{}[]>' but required in type
// '{ __memoized__: true; }'.(2322)
};
Playground link
Again, though, I don't think I'd do it that way without some strong driving factor.
DataRendererreceive a callback rather than the actual data? That would be simpler, and bypass the problem of the infinite loop.