getServerSideProps
Next.js は getServerSideProps によって取得したデータを使って page の pre-render を行う。
import { InferGetServerSidePropsType } from "next"; export const getServerSideProps = async (context) => { const res = await fetch("https://api.github.com/users/octokit"); const data = await res.json(); console.log(data); ← serverで実行される return { props: { data } }; }; const HomePage = ({ data, }: InferGetServerSidePropsType<typeof getServerSideProps>) => { console.log(data); ← ブラウザで実行される return <h1>Hello World</h1>; }; export default HomePage;
getServerSideProps は以下のいずれかの値を返す必要がある。
- props
- {props: Object}の形で渡す必要がある
- notFound
- {notFound: true}を渡すと,page は 404 を返す
- redirect
- {redirect: {destination: string, permanent: boolean}}
- redirect処理が走る
参考
useImmer
useState と同様に,state と state を更新する関数のタプル型を返す hook。state を更新する関数は,immer の produce 関数か値を引数に取れる。
immer producer function
produce
関数は以下のような interface となる。
produce(baseState, recipe: (draftState) => void): nextState
baseState
を,recipe
関数によって変更した結果を返す。しかし,baseState は immutable。
import produce from "immer"; const baseState = [ { title: "Learn TypeScript", done: true, }, { title: "Try Immer", done: false, }, ]; const nextState = produce(baseState, (draftState) => { draftState.push({ title: "Tweet about it", done: false }); draftState[1].done = true; });
Managing state with immer producer function
import { useImmer } from "use-immer"; export default function App() { const [person, updatePerson] = useImmer({ name: "Michel", age: 33, }); function updateName(name: string) { updatePerson((draft) => { draft.name = name; }); } function becomeOlder() { updatePerson((draft) => { draft.age++; }); } return ( <div className="App"> <h1> Hello {person.name} ({person.age}) </h1> <input onChange={(e) => { updateName(e.target.value); }} value={person.name} /> <br /> <button onClick={becomeOlder}>Older</button> </div> ); }
updatePerson
にperson
state を更新する関数を渡すことで,state が更新される。
Managing state as simple useState hook
import { MouseEvent } from "react"; import { useImmer } from "use-immer"; export default function App() { const [age, setAge] = useImmer(20); function birthDay(event: MouseEvent<HTMLButtonElement>) { setAge(age + 1); } return ( <div> {age} <button onClick={birthDay}>It is my birthday</button> </div> ); }
useImmer の更新関数(setAge
)に値を渡すと,useState と同じ挙動となる。
useImmerReducer
useReducerと同じことができる。
import { useImmerReducer } from "use-immer"; type State = { count: number; }; const initialState: State = { count: 0 }; type Action = { type: "reset" | "increment" | "decrement" }; function reducer(draft: State, action: Action) { switch (action.type) { case "reset": return initialState; case "increment": return void draft.count++; case "decrement": return void draft.count--; } } export default function App() { const [state, dispatch] = useImmerReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: "reset" })}>Reset</button> <button onClick={() => dispatch({ type: "increment" })}>+</button> <button onClick={() => dispatch({ type: "decrement" })}>-</button> </> ); }
参考
Updating Objects in State
Treat state as read-only
import { useState } from "react"; function App() { const [plan, setPlan] = useState({ x: 1, y: 2 }); return ( <> {plan.x} <button onClick={() => (plan.x += 1)}>a</button> </> ); } export default App;
上記のコードでボタンを押下しても,画面に表示されるplan.x
の値は変化しない。それは,state の setter 関数を使用しないと,React は object が変更されたことがわからないため。
state の setter 関数により,以下のことを React に伝える。
Using a single event handler for multiple fields
import { ChangeEvent, useState } from "react"; function App() { const [person, setPerson] = useState({ firstName: "Barbara", lastName: "Hepworth", email: "bhepworth@sculpture.com", }); function handleChange(e: ChangeEvent<HTMLInputElement>) { setPerson({ ...person, [e.target.name]: e.target.value, }); } return ( <> <label> First name: <input name="firstName" value={person.firstName} onChange={handleChange} /> </label> <label> Last name: <input name="lastName" value={person.lastName} onChange={handleChange} /> </label> <label> Email: <input name="email" value={person.email} onChange={handleChange} /> </label> <p> {person.firstName} {person.lastName} ({person.email}) </p> </> ); } export default App;
[e.target.name]: e.target.value
とすることで,動的に key を指定できる。
Updating a nested object
import { ChangeEvent, useState } from "react"; type Event = ChangeEvent<HTMLInputElement>; function App() { const [person, setPerson] = useState({ name: "Niki de Saint Phalle", artwork: { title: "Blue Nana", city: "Hamburg", image: "https://i.imgur.com/Sd1AgUOm.jpg", }, }); function handleNameChange(e: Event) { setPerson({ ...person, name: e.target.value, }); } function handleTitleChange(e: Event) { setPerson({ ...person, artwork: { ...person.artwork, title: e.target.value, }, }); } function handleCityChange(e: Event) { setPerson({ ...person, artwork: { ...person.artwork, city: e.target.value, }, }); } function handleImageChange(e: Event) { setPerson({ ...person, artwork: { ...person.artwork, image: e.target.value, }, }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {" by "} {person.name} <br /> (located in {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> ); } export default App;
nest した object でも,コピーを使用して値を更新する必要がある。
Write concise update logic with Immer
import { ChangeEvent } from "react"; import { useImmer } from "use-immer"; type Event = ChangeEvent<HTMLInputElement>; function App() { const [person, updatePerson] = useImmer({ name: "Niki de Saint Phalle", artwork: { title: "Blue Nana", city: "Hamburg", image: "https://i.imgur.com/Sd1AgUOm.jpg", }, }); function handleNameChange(e: Event) { updatePerson((draft) => { draft.name = e.target.value; }); } function handleTitleChange(e: Event) { updatePerson((draft) => { draft.artwork.title = e.target.value; }); } function handleCityChange(e: Event) { updatePerson((draft) => { draft.artwork.city = e.target.value; }); } function handleImageChange(e: Event) { updatePerson((draft) => { draft.artwork.image = e.target.value; }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {" by "} {person.name} <br /> (located in {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> ); } export default App;
useImmer
を使用することで,nest したオブジェクトの更新も楽に行える。
参考
Choosing the State Structure
state の構造を考えるときに,考慮すべき原則は以下。
- 関連する state をグループ化する。 二つ以上の state を常に同時に更新しているなら,それらの state を単一の state に統合することを考えるべき。
- state 間での矛盾を避ける。
- 冗長な state を避ける。 props や既存の state から算出できる情報を新たに state で管理すべきではない。
- 重複した state を避ける。
- 深くネストした state を避ける。 深い階層の state は更新がしにくい。
Group related state
import { useState } from "react"; function App() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={(e) => setPosition({ x: e.clientX, y: e.clientY })} style={{ position: "relative", width: "100vw", height: "100vh", }} > <div style={{ position: "absolute", backgroundColor: "red", borderRadius: "50%", transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ); } export default App;
position
のx
,y
プロパティは常に同時に更新されるため,単一の state で管理した方がよい。
Avoid contradictions in state
import { FormEvent, useState } from "react"; function App() { const [text, setText] = useState(""); const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false); async function handleSubmit(e: FormEvent<HTMLFormElement>) { e.preventDefault(); setIsSending(true); await sendMessage(text); setIsSending(false); setIsSent(true); } if (isSent) { return <h1>Thanks for feedback!</h1>; } return ( <form onSubmit={handleSubmit}> <p>How was your stay at The Pricing Pony?</p> <textarea disabled={isSending} value={text} onChange={(e) => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit"> Send </button> {isSending && <p>Sending...</p>} </form> ); } function sendMessage(text: string): Promise<void> { return new Promise((resolve) => { setTimeout(resolve, 2000); }); } export default App;
上記のコードだと,isSending
とisSent
が両方とも true という矛盾した state が存在する可能性がある。status
というユニオン型を持つ state を導入することで解決できる。
import { FormEvent, useState } from "react"; function App() { const [text, setText] = useState(""); const [status, setStatus] = (useState < "typing") | "sending" | ("sent" > "typing"); async function handleSubmit(e: FormEvent<HTMLFormElement>) { e.preventDefault(); setStatus("sending"); await sendMessage(text); setStatus("sent"); } const isSending = status === "sending"; const isSent = status === "sent"; if (isSent) { return <h1>Thanks for feedback!</h1>; } return ( <form onSubmit={handleSubmit}> <p>How was your stay at The Pricing Pony?</p> <textarea disabled={isSending} value={text} onChange={(e) => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit"> Send </button> {isSending && <p>Sending...</p>} </form> ); } function sendMessage(text: string): Promise<void> { return new Promise((resolve) => { setTimeout(resolve, 2000); }); } export default App;
typing
,sending
,sent
というユニオン型を持つstatus
state を定義することで,矛盾した state が発生しなくなった。
Avoid redundant state
import { ChangeEvent, FormEvent, useState } from "react"; function App() { const [firstName, setFirstName] = useState(""); const [lastName, setLastNaem] = useState(""); const [fullName, setFullName] = useState(""); function handleFirstNameChange(e: ChangeEvent<HTMLInputElement>) { setFirstName(e.target.value); setFullName(e.target.value + " " + lastName); } function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) { setLastNaem(e.target.value); setFullName(firstName + " " + e.target.value); } return ( <> <h2>Let's check you in</h2> <label> First name: <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); } export default App;
fullName
はfirstName
とlastName
から算出可能な値であり,冗長な state となっている。
以下のように,fullName = firstName + ' ' + lastName
でfullName
を直接算出すれば良い。
import { ChangeEvent, useState } from "react"; function App() { const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const fullName = firstName + " " + lastName; function handleFirstNameChange(e: ChangeEvent<HTMLInputElement>) { setFirstName(e.target.value); } function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) { setLastName(e.target.value); } return ( <> <h2>Let's check you in</h2> <label> First name: <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); } export default App;
Don't mirror props in state
import { useState } from "react"; function App() { const [firstName, setFirstName] = useState(""); return ( <> <label>Parent First Name</label> <input onChange={(e) => setFirstName(e.target.value)} /> <label>Child First Name</label> <FirstName firstName={firstName} /> </> ); } type FirstNameProps = { firstName: string, }; function FirstName({ firstName }: FirstNameProps) { const [name, setName] = useState(firstName); return <input value={name} onChange={(e) => setName(e.target.value)} />; } export default App;
上記のFirstName
コンポーネントは,firstName
を props として受け取り,useState の引数として受け取っている。しかし,App
コンポーネント内のfirstName
が更新され,FirstName
コンポーネントのfirstName
props が更新されても,useState の値は更新されない。なぜなら,useState の初期化は最初のレンダリングのみで行われるため。
Avoid duplication in state
import { ChangeEvent, useState } from "react"; const initialItems = [ { title: "pretzels", id: 0 }, { title: "crispy seaweed", id: 1 }, { title: "granola bar", id: 2 }, ]; function App() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState(items[0]); function handleItemChange(id: number, e: ChangeEvent<HTMLInputElement>) { setItems( items.map((item) => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } }) ); } return ( <> <h2>What's your travel snack?</h2> <ul> {items.map((item) => ( <li key={item.id}> <input value={item.title} onChange={(e) => handleItemChange(item.id, e)} />{" "} <button onClick={() => { setSelectedItem(item); }} > Choose </button> </li> ))} </ul> <p>You picked {selectedItem.title}</p> </> ); } export default App;
上記の実装だと,selectedItem
とitems
内に存在するitem
で重複が生まれてしまう。そのため,items
に変更が加えられても,selectedItem
に反映されない。selectedId
という state を持つことで state の重複を回避できる。
import { ChangeEvent, useState } from "react"; const initialItems = [ { title: "pretzels", id: 0 }, { title: "crispy seaweed", id: 1 }, { title: "granola bar", id: 2 }, ]; function App() { const [items, setItems] = useState(initialItems); const [selectedId, setSelectedId] = useState(0); const selectedItem = items.find((item) => item.id === selectedId); function handleItemChange(id: number, e: ChangeEvent<HTMLInputElement>) { setItems( items.map((item) => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } }) ); } return ( <> <h2>What's your travel snack?</h2> <ul> {items.map((item) => ( <li key={item.id}> <input value={item.title} onChange={(e) => handleItemChange(item.id, e)} />{" "} <button onClick={() => { setSelectedId(item.id); }} > Choose </button> </li> ))} </ul> {selectedItem && <p>You picked {selectedItem.title}</p>} </> ); } export default App;
Avoid deeply nested state
import { ChangeEvent, useState } from "react"; const initialTravelPlan = { id: 0, title: "(Root)", childPlaces: [ { id: 1, title: "Earth", childPlaces: [ { id: 2, title: "Africa", childPlaces: [ { id: 3, title: "Botswana", childPlaces: [], }, { id: 4, title: "Egypt", childPlaces: [], }, { id: 5, title: "Kenya", childPlaces: [], }, { id: 6, title: "Madagascar", childPlaces: [], }, { id: 7, title: "Morocco", childPlaces: [], }, { id: 8, title: "Nigeria", childPlaces: [], }, { id: 9, title: "South Africa", childPlaces: [], }, ], }, { id: 10, title: "Americas", childPlaces: [ { id: 11, title: "Argentina", childPlaces: [], }, { id: 12, title: "Brazil", childPlaces: [], }, { id: 13, title: "Barbados", childPlaces: [], }, { id: 14, title: "Canada", childPlaces: [], }, { id: 15, title: "Jamaica", childPlaces: [], }, { id: 16, title: "Mexico", childPlaces: [], }, { id: 17, title: "Trinidad and Tobago", childPlaces: [], }, { id: 18, title: "Venezuela", childPlaces: [], }, ], }, { id: 19, title: "Asia", childPlaces: [ { id: 20, title: "China", childPlaces: [], }, { id: 21, title: "Hong Kong", childPlaces: [], }, { id: 22, title: "India", childPlaces: [], }, { id: 23, title: "Singapore", childPlaces: [], }, { id: 24, title: "South Korea", childPlaces: [], }, { id: 25, title: "Thailand", childPlaces: [], }, { id: 26, title: "Vietnam", childPlaces: [], }, ], }, { id: 27, title: "Europe", childPlaces: [ { id: 28, title: "Croatia", childPlaces: [], }, { id: 29, title: "France", childPlaces: [], }, { id: 30, title: "Germany", childPlaces: [], }, { id: 31, title: "Italy", childPlaces: [], }, { id: 32, title: "Portugal", childPlaces: [], }, { id: 33, title: "Spain", childPlaces: [], }, { id: 34, title: "Turkey", childPlaces: [], }, ], }, { id: 35, title: "Oceania", childPlaces: [ { id: 36, title: "Australia", childPlaces: [], }, { id: 37, title: "Bora Bora (French Polynesia)", childPlaces: [], }, { id: 38, title: "Easter Island (Chile)", childPlaces: [], }, { id: 39, title: "Fiji", childPlaces: [], }, { id: 40, title: "Hawaii (the USA)", childPlaces: [], }, { id: 41, title: "New Zealand", childPlaces: [], }, { id: 42, title: "Vanuatu", childPlaces: [], }, ], }, ], }, { id: 43, title: "Moon", childPlaces: [ { id: 44, title: "Rheita", childPlaces: [], }, { id: 45, title: "Piccolomini", childPlaces: [], }, { id: 46, title: "Tycho", childPlaces: [], }, ], }, { id: 47, title: "Mars", childPlaces: [ { id: 48, title: "Corn Town", childPlaces: [], }, { id: 49, title: "Green Hill", childPlaces: [], }, ], }, ], }; type PlaceTreeProps = { id: number, title: string, childPlaces: PlaceTreeProps[], }; function PlaceTree({ id, title, childPlaces }: PlaceTreeProps) { return ( <> <li>{title}</li> {childPlaces.length > 0 && ( <ol> {childPlaces.map((place) => ( <PlaceTree key={place.id} id={place.id} title={place.title} childPlaces={place.childPlaces} /> ))} </ol> )} </> ); } function App() { const [plan, setPlan] = useState(initialTravelPlan); const planets = plan.childPlaces; return ( <> <h2>Places to visit</h2> <ol> {planets.map((place) => ( <PlaceTree key={place.id} id={place.id} title={place.title} childPlaces={place.childPlaces} /> ))} </ol> </> ); } export default App;
上記のような実装だと,ネストが深く更新が行いにくい。そのため,構造を以下のようにフラットにする。
import { useState } from "react"; type Plan = { id: number, title: string, childIds: number[] }; type TravelPlan = { [key: number]: Plan, }; const initialTravelPlan: TravelPlan = { 0: { id: 0, title: "(Root)", childIds: [1, 43, 47], }, 1: { id: 1, title: "Earth", childIds: [2, 10, 19, 27, 35], }, 2: { id: 2, title: "Africa", childIds: [3, 4, 5, 6, 7, 8, 9], }, 3: { id: 3, title: "Botswana", childIds: [], }, 4: { id: 4, title: "Egypt", childIds: [], }, 5: { id: 5, title: "Kenya", childIds: [], }, 6: { id: 6, title: "Madagascar", childIds: [], }, 7: { id: 7, title: "Morocco", childIds: [], }, 8: { id: 8, title: "Nigeria", childIds: [], }, 9: { id: 9, title: "South Africa", childIds: [], }, 10: { id: 10, title: "Americas", childIds: [11, 12, 13, 14, 15, 16, 17, 18], }, 11: { id: 11, title: "Argentina", childIds: [], }, 12: { id: 12, title: "Brazil", childIds: [], }, 13: { id: 13, title: "Barbados", childIds: [], }, 14: { id: 14, title: "Canada", childIds: [], }, 15: { id: 15, title: "Jamaica", childIds: [], }, 16: { id: 16, title: "Mexico", childIds: [], }, 17: { id: 17, title: "Trinidad and Tobago", childIds: [], }, 18: { id: 18, title: "Venezuela", childIds: [], }, 19: { id: 19, title: "Asia", childIds: [20, 21, 22, 23, 24, 25, 26], }, 20: { id: 20, title: "China", childIds: [], }, 21: { id: 21, title: "Hong Kong", childIds: [], }, 22: { id: 22, title: "India", childIds: [], }, 23: { id: 23, title: "Singapore", childIds: [], }, 24: { id: 24, title: "South Korea", childIds: [], }, 25: { id: 25, title: "Thailand", childIds: [], }, 26: { id: 26, title: "Vietnam", childIds: [], }, 27: { id: 27, title: "Europe", childIds: [28, 29, 30, 31, 32, 33, 34], }, 28: { id: 28, title: "Croatia", childIds: [], }, 29: { id: 29, title: "France", childIds: [], }, 30: { id: 30, title: "Germany", childIds: [], }, 31: { id: 31, title: "Italy", childIds: [], }, 32: { id: 32, title: "Portugal", childIds: [], }, 33: { id: 33, title: "Spain", childIds: [], }, 34: { id: 34, title: "Turkey", childIds: [], }, 35: { id: 35, title: "Oceania", childIds: [36, 37, 38, 39, 40, 41, 42], }, 36: { id: 36, title: "Australia", childIds: [], }, 37: { id: 37, title: "Bora Bora (French Polynesia)", childIds: [], }, 38: { id: 38, title: "Easter Island (Chile)", childIds: [], }, 39: { id: 39, title: "Fiji", childIds: [], }, 40: { id: 40, title: "Hawaii (the USA)", childIds: [], }, 41: { id: 41, title: "New Zealand", childIds: [], }, 42: { id: 42, title: "Vanuatu", childIds: [], }, 43: { id: 43, title: "Moon", childIds: [44, 45, 46], }, 44: { id: 44, title: "Rheita", childIds: [], }, 45: { id: 45, title: "Piccolomini", childIds: [], }, 46: { id: 46, title: "Tycho", childIds: [], }, 47: { id: 47, title: "Mars", childIds: [48, 49], }, 48: { id: 48, title: "Corn Town", childIds: [], }, 49: { id: 49, title: "Green Hill", childIds: [], }, }; type PlaceTreeProps = { id: number, parentId: number, placesById: TravelPlan, onComplete: (parentId: number, id: number) => void, }; function PlaceTree({ id, parentId, placesById, onComplete }: PlaceTreeProps) { const place = placesById[id]; const childIds = place.childIds; return ( <> <li> {place.title} <button onClick={() => onComplete(parentId, id)}>Complete</button> </li> {childIds.length > 0 && ( <ol> {childIds.map((childId) => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> )} </> ); } function App() { const [plan, setPlan] = useState(initialTravelPlan); const root = plan[0]; const planetIds = root.childIds; function handleComplete(parentId: number, childId: number) { const parent = plan[parentId]; const nextParent: Plan = { ...parent, childIds: parent.childIds.filter((id) => id !== childId), }; setPlan({ ...plan, [parentId]: nextParent, }); } return ( <> <h2>Places to visit</h2> <ol> {planetIds.map((id) => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> ); } export default App;
2022/07/21
readonly
readonly のチェックがされない時がある
const sum = (obj: { foo: number; bar: number; baz: number }) => { obj.foo = 999999; return obj.foo + obj.bar + obj.baz; }; const myObj = { foo: 0, bar: 100, baz: 1000, } as const; sum(myObj); ← sum内でfooの書き換えが行われているのに,コンパイルエラーとならない
配列における readonly
const sum = (arr: number[]) => { return arr.reduce((a, b) => a + b, 0); }; const myArr = [1, 2, 3, 4] as const; sum(myArr); ← sum内でarrの書き換えは行わないのに,readonlyに関してのコンパイルエラーとなる
参考
2022/07/19
mapped type
readonly
型をつけることで,書き換え不可能なものであるというインターフェースだとわかる
const convertToNumberAll = (strArray: readonly string[]): number[] => { return strArray.map((str) => parseInt(str, 10)); }; const array = ["1", "2"] as const; const intArray = convertToNumberAll(array);
- mapped type
const convertToNumberAll = <A extends readonly string[]>( strArray: A ): { [K in keyof A]: number } => { return strArray.map((str) => parseInt(str, 10)) as any; }; const array = ["1", "2"] as const; const intArray = convertToNumberAll(array);
要素数が引数と返り値で同じだと型からわかる
conditional type
type Options<IsOptional extends boolean> = { optional: IsOptional }; const getUserInput = async <IsOptional extends boolean>({ optional, }: Options<IsOptional>): Promise< IsOptional extends false ? string : string | undefined > => { while (true) { const result = await fetch("/"); if (result !== undefined || !optional) { return result.json(); } } }; const foo = getUserInput({ optional: false }); => Promise<string> const bar = getUserInput({ optional: true }); => Promise<string | undefined>