简体   繁体   中英

TypeScript types to infer via keyof within React Context Provider

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!

TypeScript playground demo

 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:

编辑 React Typescript(分叉)

function App() {
  const sports = useSportsContext();

  useEffect(() => {
    if (sports) {
      const cricket = sports.getSportInfo("cricket");
      console.log(cricket);
    }
  }, [sports]);

  return (
    <SportsProvider>
      <div>App</div>
    </SportsProvider>
  );
}

UPDATED

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM