Arihant
Arihant

Reputation: 497

Typescript: How To Use Generics Properly To Infer Return Type of Function Correctly?

I've the following types

type ItemDefaultType = object | null | string

interface ItemToString<Item = ItemDefaultType> {
  (item: ItemDefaultType): string;
}

interface AutosuggestState<Item = ItemDefaultType> {
  highlightedIndex: number | null
  inputValue: string | null
  isOpen: boolean
  selectedItem: Item
}

interface AutosuggestProps<Item = ItemDefaultType> extends AutosuggestState<Item> {
  itemToString?: ItemToString<Item>;

  initialSelectedItem?: Item;
  initialInputValue?: string;
  initialHighlightedIndex?: number | null;
  initialIsOpen?: boolean;

  defaultHighlightedIndex?: number | null;
  defaultIsOpen?: boolean;

  id?: string;
  inputId?: string;
  labelId?: string;
  menuId?: string;

  itemCount?: number
}

I want to make a getInitialValue function with the intended signature –

// this has to be done correctly using generics, not been able to do it, read on…
(
  props: Props extends AutosuggestProps,
  stateKey: StateKey extends keyof AutosuggestState
) => AutosuggestState[StateKey] // return the correct type of AutosuggestState property based on which stateKey was passed

The behaviour of getInitialValue looks like this

// javascript version

const defaultStateValues = {
  highlightedIndex: -1,
  isOpen: false,
  selectedItem: null,
  inputValue: ''
}

function getDefaultValue(props, statePropKey) {
  const defaultPropKey = `default${capitalizeString(statePropKey)}`
  if (defaultPropKey in props) {
    return props[defaultPropKey]
  }
  return defaultStateValues[statePropKey]
}

function getInitialValue(props, statePropKey) {
  if (statePropKey in props) {
    return props[statePropKey]
  }
  const initialPropKey = `initial${capitalizeString(statePropKey)}`
  if (initialPropKey in props) {
    return props[initialPropKey]
  }
  return getDefaultValue(props, statePropKey)
}

I'm finding it hard to write types of both getInitialValue and getDefaultValue such that getInitialValue correctly infers the right type as below –

const selectedItem = getInitialValue(props, 'selectedItem') // selectedItem variable should correctly be inferred as **object | null | string** since that's what its type is in **AutosuggestState** interface

Can someone help me write the types?

Upvotes: 0

Views: 126

Answers (1)

jcalz
jcalz

Reputation: 330086

Here's a possible typing, which might or might not fit your use case:

function getDefaultValue<P extends AutosuggestProps, K extends keyof AutosuggestState>(
  props: P,
  statePropKey: K) {
  const defaultPropKey = `default${capitalizeString(statePropKey)}`
  if (defaultPropKey in props) {
    return props[defaultPropKey as K] // assert here
  }
  return defaultStateValues[statePropKey]
}

function getInitialValue<P extends AutosuggestProps, K extends keyof AutosuggestState>(
  props: P, statePropKey: K) {
  if (statePropKey in props) {
    return props[statePropKey]
  }
  const initialPropKey = `initial${capitalizeString(statePropKey)}`
  if (initialPropKey in props) {
    return props[initialPropKey as K] // assert here
  }
  return getDefaultValue(props, statePropKey)
}

All I've really done is make the functions generic in P and K of the types you alluded to in your pseudocode.

One decision you need to make is what to do when you use defaultPropKey and initialPropKey. The compiler has no way to verify that prepending "default" or "initial" to a property name will, if it exists, pick out another property of the same type. The compiler does not understand string concatenation at the type level (the suggestion for doing so in Github seems to be microsoft/TypeScript#12754). If you could refactor your interfaces so that the default and initial values are stored in properties named default and initial which are themselves objects holding properties of the same keys, then you could make the compiler figure it out. Assuming that you're keeping it this way, I've used a type assertion to say, for example, "treat initialPropKey as if it were of type K". Then the compiler will assume that props[initialPropKey] is of the same type as props[statePropKey].

Anyway, now the return types of those functions are inferred as

P[K] | {
    highlightedIndex: number;
    isOpen: boolean;
    selectedItem: null;
    inputValue: string;
}[K]

which makes sense since it uses defaultStateValues at the end, which is a concrete type not known to be the extended P type. This might be tightened up by using a conditional return type, but your example code didn't show a need for that.

Let's see if it works:

declare const props: AutosuggestProps;
const selectedItem = getInitialValue(props, 'selectedItem');
// const selectedItem: ItemDefaultType

Looks reasonable to me. Okay, hope that helps; good luck!

Link to code

Upvotes: 1

Related Questions