I have setup the TypeScript types to infer the value based on an object key using keyof. They appear to work outside of the React Context Provider however I am having problems when using similar functions inside the context provider. The TypeScript Playground link demonstrates the inference error best.
I figure I am missing Type annotations somewhere but cannot find any solutions that work. Any assistance appreciated!
type IndividualSport = { name: string; } type TeamSport = { name: string; players: number; } type Sports = { cricket: TeamSport; golf: IndividualSport; } type SportsKey = keyof Sports; let sampleSports: Sports = { cricket: { name: "Cricket", players: 11 }, golf: { name: "Golf", }, }; function extractSport<K extends SportsKey>(sports: Sports, key: K) { return sports[key]; } const cricket = extractSport(sampleSports, 'cricket'); const golf = extractSport(sampleSports, 'golf'); // THIS WORKS console.log(cricket.players); console.log(golf.name); // REACT CONTEXT PROVIDER type SportsContextProps<K extends SportsKey> = { getSportInfo: (key: K) => Sports[K]; } const SportsContext = createContext<SportsContextProps<SportsKey>>({ getSportInfo: (key) => extractSport(sampleSports, key), }); const useSportsContext = () => useContext(SportsContext)!; function SportsProvider({ children, }: PropsWithChildren<{}>): ReactElement { const [sports, setSports] = useState<Sports>(sampleSports); function getSportInfo(key: SportsKey): Sports[SportsKey] { return extractSport(sports, key); } return ( <SportsContext.Provider value={{ getSportInfo, }} > {children} </SportsContext.Provider> ); } function App() { const { getSportInfo } = useSportsContext(); // Why is cricket not inferred correctly from the context? const cricket = getSportInfo('cricket'); console.log(cricket.name); console.log(cricket.players); // THIS FAILS as cricket is inferred as TeamSport | IndividualSport return ( <div>App</div> ); } ReactDOM.render( <React.StrictMode> <SportsProvider> <App /> </SportsProvider> </React.StrictMode>, document.getElementById('root') );
First, you don't have to create IndividualSport
and TeamSport
. You can create an intersection types :
type Sport = {
name: string;
players?: number; // don't forget the question mark
};
But if you intentionally create two types, just extend an interface:
interface IndividualSport {
name: string;
}
interface TeamSport extends IndividualSport {
players: number;
}
But in your case, you just need one type which is Sport
. So, your Sports
types become:
type Sports = {
cricket: Sport;
golf: Sport;
};
Next, I think we don't have to make the key generics. If you write SportsKey is keyof
Sports, the SportsKey
alreadystring or numeric literal union of Sports
keys
type SportsKey = keyof Sports;
// idem with:
type SportsKey = 'name' | 'players'
So, your context creation becomes:
function extractSport(sports: Sports, key: SportsKey) {
return sports[key];
}
type SportsContextProps = {
getSportInfo: (key: SportsKey) => Sports[SportsKey];
};
const SportsContext = createContext<SportsContextProps>({
getSportInfo: (key: SportsKey) => extractSport(sampleSports, key),
});
const useSportsContext = () => useContext(SportsContext);
function SportsProvider({ children }: PropsWithChildren<{}>): ReactElement {
const [sports] = useState<Sports>(sampleSports);
function getSportInfo(key: SportsKey): Sports[SportsKey] {
return extractSport(sports, key);
}
return (
<SportsContext.Provider
value={{
getSportInfo,
}}
>
{children}
</SportsContext.Provider>
);
}
Finally, you can access your sport context and log the result as follow:
function App() {
const sports = useSportsContext();
useEffect(() => {
if (sports) {
const cricket = sports.getSportInfo("cricket");
console.log(cricket);
}
}, [sports]);
return (
<SportsProvider>
<div>App</div>
</SportsProvider>
);
}
But if you still want to use your approach, the short solution is you need to cast the cricket
as TeamSport
even though the type of cricket already:
const cricket: TeamSport | IndividualSport
// which is comes from:
const getSportInfo: (key: keyof Sports) => IndividualSport | TeamSport
The weakness of your approach is that you will always be asked to manually cast both the cricket and golf to tell typescript which type that will be used.
function App() {
const { getSportInfo } = useSportsContext();
useEffect(() => {
const cricket = getSportInfo("cricket");
const { name, players } = cricket as TeamSport;
console.log(name, players);
}, [])
return (
<div>App</div>
);
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.