Home > Back-end >  how to type narrow an object with arrays grouped by category when all items within each array must h
how to type narrow an object with arrays grouped by category when all items within each array must h

Time:07-19

Hi I am quite new to typescript and I would like to type an object which contains arrays grouped by category, and I would like to type according to this.

const machines:IMachines = { 
  1: [{category:"core", serial:"110"}, {category:"core",serial:231}],
  2: [{category:"style", serial:"114"}, {category:"style",serial:"239"}]
}

type IMachines = {
  [key:number]:IMachine[]
}

type IMachine = {
  category:string
  serial: string
}

From here I would like to narrow the type, to make sure all Machines within the same array have the same "category" type.

For example, if I change the machine object so the machine with serial 239, has now type "core". That should throw TS error, because style and core are different cateogry types within the same array.

const machines:IMachines = { 
  1: [{category:"core", serial:"110"}, {category:"core",serial:231}],
  2: [{category:"style", serial:"114"}, {category:"core",serial:"239"}]
}

==>This should throw error

I am not sure how to do it:

I would attempt something like:

type IMachines<T> = {
  [key:number]:IMachine<T>[]
}

type IMachine<T> = {
  category:TCategory<T>
  serial: string
}

type TCategory<K> = K

This doesn't work but, I posted just to explain the idea of what I am trying to achieve.

Thank you very much

CodePudding user response:

That's an interesting problem. TypeScripts stand-alone types are not able to validate complex constraints like this. It is possible to achieve this with generics but you would have to explicitly provide a list of valid category names to the generic type.


It is also possible to validate objects like this with generic functions. A possible implementation would look like this.

type UnionToIntersection<U> = 
    (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never 

type NoUnion<Key> =
    [Key] extends [UnionToIntersection<Key>] ? Key : never; 

function validateMachines<
  T extends Record<string, string>
>(machines: { [K in keyof T]: { category: NoUnion<T[K]>, serial: string }[] }) {
    return machines
}

If you call this with your machines objects, they will be properly validated.

const res1 = validateMachines({ 
    1: [{category:"core", serial:"110"}, {category:"core",serial:"231"}], 
    2: [{category:"style", serial:"114"}, {category:"style",serial:"239"}]
})

const res2 = validateMachines({ 
    1: [{category:"core", serial:"110"}, {category:"core",serial:"231"}],
    2: [{category:"style", serial:"114"}, {category:"core",serial:"239"}] // Error
})

Playground


If you have a predefined enum, a solution would look like this:

enum Categories {
    core = "core",
    style = "style"
}

type IMachines = Record<number, {
    -readonly [K in keyof typeof Categories]: { category: K, serial: string }[]
}[keyof typeof Categories]>


const machines: IMachines = { 
    1: [{category:"core", serial:"110"}, {category:"core",serial:"231"}],
    2: [{category:"style", serial:"114"}, {category:"core",serial:"239"}] 
//  ^Error
}

Playground

  • Related