Dynamic record with value type depending on key suffix

I need to create a list of records for a scatter chart data with error bars. There can be an arbitrary number of datasets in this chart. The record contains a x key representing the x-axis position (value type number), ${datasetID}_value key representing the value of the dataset with given id (value type number) and ${datasetID}_errors representing the errors of the value at this position (type [number, number]). I want to be able to push a valid record into the list without TS errors like so
type DataRecord = ??? // TODO: define this
type DataList = DataRecord[]

const myData: DataList = []

// goal is for this to work
myData.push({
x: 0,
[`${id1}_value`]: 1,
[`${id1}_error`]: [0, 1],
})

// this should also work
myData.push({
x: 0,
[`${id1}_value`]: 1,
[`${id1}_error`]: [0, 1],
[`${id2}_value`]: 2,
[`${id2}_error`]: [1, 1],
})
type DataRecord = ??? // TODO: define this
type DataList = DataRecord[]

const myData: DataList = []

// goal is for this to work
myData.push({
x: 0,
[`${id1}_value`]: 1,
[`${id1}_error`]: [0, 1],
})

// this should also work
myData.push({
x: 0,
[`${id1}_value`]: 1,
[`${id1}_error`]: [0, 1],
[`${id2}_value`]: 2,
[`${id2}_error`]: [1, 1],
})
I'm not sure if this is possible since the prop inside the push() call is being treated as {x: number; [x: string]: number | number[]}, so I'd love to get help on this, thank you so much. Here is my progress so far: TS Playground
Solution:
i think your problem is that id needs to be a string literal (not just type string) ``typescript type DataRecord = { [key: ${string}_value`]: number...
Jump to solution
6 Replies
Solution
erik.gh
erik.gh9mo ago
i think your problem is that id needs to be a string literal (not just type string)
type DataRecord = {
[key: `${string}_value`]: number
[key: `${string}_error`]: [number, number],
x: number
}

type DataList = DataRecord[]

const myData: DataList = []

const id1 = 'key1'
const id2 = 'key2'

// goal is for this to work
myData.push({
x: 0,
[`${id1}_value`]: 1,
[`${id1}_error`]: [0, 1],
})

// this should also work
myData.push({
x: 0,
[`${id1}_value`]: 1,
[`${id1}_error`]: [0, 1],
[`${id2}_value`]: 2,
[`${id2}_error`]: [1, 1],
})
type DataRecord = {
[key: `${string}_value`]: number
[key: `${string}_error`]: [number, number],
x: number
}

type DataList = DataRecord[]

const myData: DataList = []

const id1 = 'key1'
const id2 = 'key2'

// goal is for this to work
myData.push({
x: 0,
[`${id1}_value`]: 1,
[`${id1}_error`]: [0, 1],
})

// this should also work
myData.push({
x: 0,
[`${id1}_value`]: 1,
[`${id1}_error`]: [0, 1],
[`${id2}_value`]: 2,
[`${id2}_error`]: [1, 1],
})
this works because typeof key1 and key2 is inferred as literal key1 and key2 (as it's a const assignment). this way ts knows that the same key is not defined twice.
cornflour
cornflourOP9mo ago
yeah it's rough since the id is a dynamic string, so i don't know them beforehand but your response did allow me to get a bit farther by lying to TS:
type DataRecord = {
[key: `${string}_value`]: number
[key: `${string}_error`]: [number, number],
x: number
}

type DataList = DataRecord[]

const myData: DataList = []

// mock that the id is dynamic
// in reality this is part of the server response
const getId = () => {
return `id${Math.random() * 10}`
}

// lie to typescript here to pretend that this is a string literal
const id1 = getId() as 'id'
const id2 = getId() as 'id'

// this now works, though it needs the `as const` which is slightly annoying
myData.push({
x: 0,
[`${id1}_value` as const]: 1,
[`${id1}_error` as const]: [0, 1],
})

myData.push({
x: 0,
[`${id1}_value` as const]: 1,
[`${id1}_error` as const]: [0, 1],

// this fails because typescript thinks both id1 and id2 are literal "id", making the keys the same
// if we use a different string literal to pretend id2 as, there will be no more errors
[`${id2}_value` as const]: 2,
[`${id2}_error` as const]: [1, 1],
})
type DataRecord = {
[key: `${string}_value`]: number
[key: `${string}_error`]: [number, number],
x: number
}

type DataList = DataRecord[]

const myData: DataList = []

// mock that the id is dynamic
// in reality this is part of the server response
const getId = () => {
return `id${Math.random() * 10}`
}

// lie to typescript here to pretend that this is a string literal
const id1 = getId() as 'id'
const id2 = getId() as 'id'

// this now works, though it needs the `as const` which is slightly annoying
myData.push({
x: 0,
[`${id1}_value` as const]: 1,
[`${id1}_error` as const]: [0, 1],
})

myData.push({
x: 0,
[`${id1}_value` as const]: 1,
[`${id1}_error` as const]: [0, 1],

// this fails because typescript thinks both id1 and id2 are literal "id", making the keys the same
// if we use a different string literal to pretend id2 as, there will be no more errors
[`${id2}_value` as const]: 2,
[`${id2}_error` as const]: [1, 1],
})
for the code i was working at, this is sufficient because i apparently never had to put 2 _value keys into 1 object at any time, but even then it was still a bit annoying with the as "id" and as const.
erik.gh
erik.gh9mo ago
would you mind sharing the code for the real getId function maybe there is some generic magic we could do
cornflour
cornflourOP9mo ago
there is no real getId() function, it's just an id from server response. In actuality, I needed 3 different type of key suffixes, the third one being _trend for drawing the trendline. My code is something like this:
type DataRecord = {
[`${string}_value`]: number
[`${string}_error`]: [number, number]
[`${string}_trend`]: number
}
type DataList = ({x: number} & DataRecord)[]

// record with key being the x axis position, and the value being all the points in this position
const chartDataRecord: Record<number, DataRecord> = {}

/*
typeof datasets = {
id: string
entries: {
x: number
value: number
error: [number, number]
}[]
}[]
*/
for (const dataset of datasets) {
for (const entry of dataset.entries) {

// put all entries with the same x position across different records into the same object
if (!chartDataRecord[entry.x]) {
chartDataRecord[entry.x] = {}
}
chartDataRecord[entry.x][`${dataset.id}_value`] = entry.value
chartDataRecord[entry.x][`${dataset.id}_error`] = entry.errors
}
}

// turn records to array
for (const x in chartDataRecord) {
myData.push({
x,
...chartDataRecord[x]
})
}

for (const dataset in datasets) {
const {point1, point2} = calculateTrendline(dataset)

const datasetID = dataset.id as "id" // hack TS type
myData.push({
x: point1.x,
[`${datasetID}_trend` as const]: point1.y
})
myData.push({
x: point2.x
[`${datasetID}_trend` as const]: point2.y
})
}
type DataRecord = {
[`${string}_value`]: number
[`${string}_error`]: [number, number]
[`${string}_trend`]: number
}
type DataList = ({x: number} & DataRecord)[]

// record with key being the x axis position, and the value being all the points in this position
const chartDataRecord: Record<number, DataRecord> = {}

/*
typeof datasets = {
id: string
entries: {
x: number
value: number
error: [number, number]
}[]
}[]
*/
for (const dataset of datasets) {
for (const entry of dataset.entries) {

// put all entries with the same x position across different records into the same object
if (!chartDataRecord[entry.x]) {
chartDataRecord[entry.x] = {}
}
chartDataRecord[entry.x][`${dataset.id}_value`] = entry.value
chartDataRecord[entry.x][`${dataset.id}_error`] = entry.errors
}
}

// turn records to array
for (const x in chartDataRecord) {
myData.push({
x,
...chartDataRecord[x]
})
}

for (const dataset in datasets) {
const {point1, point2} = calculateTrendline(dataset)

const datasetID = dataset.id as "id" // hack TS type
myData.push({
x: point1.x,
[`${datasetID}_trend` as const]: point1.y
})
myData.push({
x: point2.x
[`${datasetID}_trend` as const]: point2.y
})
}
this is why i said I never had to put 2 _value in 1 object at a time, since i used a for loop to add one entry to the record at a time. But i needed the as "id" and as const hack to add the trendline points to the list, which is annoying
erik.gh
erik.gh9mo ago
are you in control of what a dataset looks like? going for an array or a map might be a smoother solution than relying on dynamically set keys…
cornflour
cornflourOP9mo ago
you mean if it is possible for me to change the final shape? at first i had it as nested instead of the weird suffix like this, but i think i ran into some trouble with the chart library :/ Maybe i can dig into deeper to see if it can work, but at this point i feel like its more effort than its worth since everything is working now (even if the as "id" and as const are slightly annoying) at this point i'm moreso curious about whether this can theoretically be done in TS than needing it for my actual task. I think i'll close the question for now
Want results from more Discord servers?
Add your server