Ahmad
Ahmad

Reputation: 2707

Typescript typeof value in an object

Trying to strongly type a data formatting function. What function does is simple,

Input:

const eleOfArray = {
  a: 1,
  b: "string",
  c: [1, 2, 3]
}

Output:

const eleOfArray = {
  a: {
    value: 1,
    component: 1
  },
  b: {
    value: "string",
    component: "string"
  },
  c: {
    value: [1, 2, 3],
    component: [1, 2, 3]
  }
}

function:

export function tableDataFormatter<T>(d: T) {
	const formatedData: ITableData<T> = {}

	Object.keys(d).forEach((key: string) => {
		// tslint:disable-next-line:ban-ts-ignore
		// @ts-ignore
		formatedData[key] = {
			// tslint:disable-next-line:ban-ts-ignore
			// @ts-ignore
			value: d[key as keyof T],
			component: d[key as keyof T]
		}
	})

	return formatedData
}

Interface:

interface ITableData<T> {
	readonly [key: keyof T]: {
		readonly component: React.ReactNode
		readonly value: T[keyof T]
	}
}

The problem I'm having with this code is when I use tableDataFormatter it shows value is always string | number.

Usage:

public formatData<IBenefit> (data: ReadonlyArray<IBenefit>): ReadonlyArray<ITableData<IBenefit>> {
		return super.formatData(data).map((d: ITableData<IBenefit>) => ({
			...d,
			stores: {
				...d.stores,
				component: <Addon addonValues={d.stores.value} />
        // d.stores.value should be an Array but it's being shown as ReactText/string
    }
  }))
}

So I have to suppress the err cause the function is working as intended and I'm clearly assigning value as readonly value: T[keyof T]

Upvotes: 1

Views: 5125

Answers (1)

jcalz
jcalz

Reputation: 328608

Your ITableData<T> interface is not valid TypeScript. Specifically, you are trying to use an index signature constrained to keyof T, but the only allowable index signature types are string and number. Instead you should consider using a mapped type instead of an interface. It will give you the mapping you want:

type ITableData<T> = {
  readonly [K in keyof T]: {
    readonly component: React.ReactNode
    readonly value: T[K]
  }
}

Note how the nested value property is of type T[K] and not T[keyof T]. T[keyof T] is going to be the union of all value types of T and the mapping from each key to each value is lost. But T[K] means that for each key K, the nested value property is of the same type as the original property of T indexed by K. This is the way to avoid the string | number problem.

Then for tableDataFormatter() I'd change some of the annotations and assertions like follows:

// strip off the readonly modifier for the top level properties
type Mutable<T> = { -readonly [K in keyof T]: T[K] };

// explicitly declare that the function returns ITableData<T>
export function tableDataFormatter<T extends Record<keyof T, React.ReactNode>>(
  d: T
): ITableData<T> {

  // assert that the return value will be ITableData<T> but make it
  // mutable so we can assign to it in the function without error
  const formatedData = {} as Mutable<ITableData<T>>;

  // assert that Object.keys(d) returns an array of keyof T types
  // this is not generally safe but is the same as your "as keyof T"
  // assertions and only needs to be done once.
  (Object.keys(d) as Array<keyof T>).forEach(key => {
    formatedData[key] = {
      value: d[key],
      component: d[key]
    }
  })

  return formatedData
}

Hopefully that works the way you expect now. Good luck!

Upvotes: 3

Related Questions