简体   繁体   中英

React Typescript Generic Type Overloading (e.g. useState)

I am attempting to create an overloaded function that will write to and from session storage instead of the standard state. I'd like it to be overloadable in the instances where I only provide a type and not an initial value. Exactly like what useState does in react.

Here is what I have so far:

export function useSessionState<S = undefined>(initialState?: S): [S, (state: S) => void];
export function useSessionState<S>(initialState: S): [S, (state: S) => void] {
  const [state, setState] = useState<S>()

  // Unimportant: Do something to load to and from session storage.

  return [state, (value) => {

  }]
}

When I try to consume this type I only ever get the result type of one of the overloads.

// Variable 'state' is a string as expected.
const [state, setState] = useSessionState("some_initial_value")

// Variable 'undefinedState' is also a string, but it should be undefined since no 'initialState' was provided.
const [undefinedState, setUndefinedState] = useSessionState<string>()

I didn't know the answer myself, so I started reading and experimenting. My attempts come pretty close, but I see the following flaws:

  • Both attempts show initialState as being of type S instead of S | undefined S | undefined , which means you'd strugle to implement this function without a cast.
  • The generated .d.ts of the first solution and the second solution are different. I believe the .d.ts of the first attempt more closely resembles the official useState definition.
  • Even though you didn't mention it:
    • initialState can be a value or a function, which is only the case in the second attempt.
    • The setState function should probably have a parameter of type S | undefined S | undefined , rather than S .

Hopefully there is a better solution... If only the actual implementation was written in Typescript.

Attempt 1: Conditional types

Playground

function useState<S>(initialState: S): [S, (state: S) => void];
function useState<S = undefined>(): [S | undefined, (state: S) => void];
function useState<S = undefined>(initialState?: S): [S extends undefined ? undefined | S : S, (state: S) => void] {
  throw new Error(`Not implemented and not using ${initialState}`);
}

const [state1, setState1] = useState<string>("initialValue"); // string
const [state2, setState2] = useState("initialValue"); // string
const [state3, setState3] = useState<string>(); // undefined | string
const [state4, setState4] = useState(); // undefined

The .d.ts that is generated on the playground:

declare function useState<S>(initialState: S): [S, (state: S) => void];
declare function useState<S = undefined>(): [S | undefined, (state: S) => void];

Attempt 2: Without conditional types

Playground

type Dispatch<T> = (value: T) => void;

function useState<S = undefined>(): [S | undefined, Dispatch<S | undefined>];
function useState<S>(initialState?: S | (() => S)): [S, Dispatch<S>];
function useState<S>(initialState?: S | (() => S)): [S, Dispatch<S>] {
    throw new Error(`Not implemented and not using ${initialState}`);
}
 
const [state1, setState1] = useState<string>("initialValue"); // string
const [state2, setState2] = useState("initialValue"); // string
const [state3, setState3] = useState<string>(); // undefined | string
const [state4, setState4] = useState(); // undefined

The .d.ts that is generated on the playground:

declare function useState<S = undefined>(): [S | undefined, Dispatch<S | undefined>];
declare function useState<S>(initialState?: S | (() => S)): [S, Dispatch<S>];

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