I have some data in my code like below:
interface Product {
id: number
name: string;
}
enum EnumValue {
'VALUE1' = 'VALUE1',
'VALUE2' = 'VALUE2',
'VALUE3' = 'VALUE3',
}
const data = {
'VALUE1': {
num1: {id: 1, name: '2'},
num2: {id: 2, name: '2'},
},
'VALUE2': {
num1: {id: 1, name: '2'},
},
'VALUE3': {
num1: {id: 1, name: '2'},
},
} as const satisfies { readonly [key in EnumValue]: { [key: string]: Product} };
I need to Define a validator for my data type so it only takes unique ids for each EnumValue property. I mean
data = {
'VALUE1': {
num1: {id: 1, name: '2'},
num2: {id: 1, name: '2'},
},
'VALUE2': {
num1: {id: 1, name: '2'},
},
'VALUE3': {
num1: {id: 1, name: '2'},
},
}
ts should throw error because VALUE1 has 2 objects with id = 1 but
data = {
'VALUE1': {
num1: {id: 1, name: '2'},
num2: {id: 2, name: '2'},
},
'VALUE2': {
num1: {id: 1, name: '2'},
},
'VALUE3': {
num1: {id: 1, name: '2'},
},
is a valid value.
i need as const satisfies part to use data model type in my code.
so can you help me define a validator to correct my data type?
There is some code to validate unique ids over an array of objects that may help but the problem is i don't know how to access object values to iterate on in type validation. link to this question
interface IProduct<Id extends number> {
id: Id
name: string;
}
type Validation<
Products extends IProduct<number>[],
Accumulator extends IProduct<number>[] = []>
=
(Products extends []
// #1 Last call
? Accumulator
// #2 All calls but last
: (Products extends [infer Head, ...infer Tail]
? (Head extends IProduct<number>
// #3 Check whether [id] property already exists in our accumulator
? (Head['id'] extends Accumulator[number]['id']
? (Tail extends IProduct<number>[]
// #4 [id] property is a duplicate, hence we need to replace it with [never] in order to trigger the error
? Validation<Tail, [...Accumulator, { id: never, name: Head['name'] }]>
: 1)
// #5 [id] is not a duplicate, hence we can add to our accumulator whole product
: (Tail extends IProduct<number>[]
? Validation<Tail, [...Accumulator, Head]>
: 2)
)
: 3)
: Products)
)
There is no specific type
ValidDatain TypeScript corresponding to your requirement that each property have uniqueidsubproperties, so you can't writeconst data = {⋯} as const satisfies ValidData. Instead you can make a genericValidData<T>type that checks the input typeTand validates it, along with a helpervalidData()function. So you'd writeconst data = validData({⋯});and it would either succeed or fail based on the input.First lets write a
BasicData<K>type to represent a supertype of allValidData<T>types, where we don't care about the uniqueness of theid; all we care about is that it has keysKand values whose properties are allProducts:Note that I've used a default generic type argument so that
BasicDataby itself corresponds toBasicData<EnumValue>, which is basically the same as what you were using aftersatisfies.Then,
ValidData<T>would look likewhere
UniqueId<T>is a validator generic that makes sureThas uniqueidproperties. Here's one way to write that:This is a mapped type over
Twhere each property is aProductwhoseidproperty explicitlyExcludes theidproperties from all other keys.T[Exclude<keyof T, K>]is the union of all properties ofTexcept the one with keyK. And so thatidproperty looks like "Take theidproperty of this property andExcludetheidproperties from all other properties."If the
idproperties are unique, this will end up not excluding anything. If they are not, then theidproperty for the duplicates will end up being thenevertype. So ifUniqueId<T>extendsT, thenTis valid. Otherwise,Tis invalid in exactly those properties with duplicateids.So now we can write
validData()like this:This uses a
consttype parameter so that callers don't need to remember to use aconstassertion. The constraint is the intersection of all the individual constraints we care about. The first isBasicData, meaning it must have all the keys ofEnumValue. The second isBasicData<keyof T>, meaning for each property it does have, all of the properties must be a bag ofProducts. And the final one isValidData<T>, meaning that its properties must have uniqueids.Okay, let's test it:
Looks good. For
data1everything passes. Fordata2, the'VALUE1'property fails, and theidproperties for bothnum1andnum2have errors saying that they are not assignable tonever. It's not the prettiest error message, but it works.You could do some complicated mess to try to make the error message more understandable:
But I consider that out of scope for this question and I won't digress further by explaining it here.
Playground link to code