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)
)
2
Answers
Tnx to @jcalz this question is answered. the solution is implemented in this link
There is no specific type
ValidData
in TypeScript corresponding to your requirement that each property have uniqueid
subproperties, so you can’t writeconst data = {⋯} as const satisfies ValidData
. Instead you can make a genericValidData<T>
type that checks the input typeT
and 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 keysK
and values whose properties are allProduct
s:Note that I’ve used a default generic type argument so that
BasicData
by 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 sureT
has uniqueid
properties. Here’s one way to write that:This is a mapped type over
T
where each property is aProduct
whoseid
property explicitlyExclude
s theid
properties from all other keys.T[Exclude<keyof T, K>]
is the union of all properties ofT
except the one with keyK
. And so thatid
property looks like "Take theid
property of this property andExclude
theid
properties from all other properties."If the
id
properties are unique, this will end up not excluding anything. If they are not, then theid
property for the duplicates will end up being thenever
type. So ifUniqueId<T>
extendsT
, thenT
is valid. Otherwise,T
is invalid in exactly those properties with duplicateid
s.So now we can write
validData()
like this:This uses a
const
type parameter so that callers don’t need to remember to use aconst
assertion. 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 ofProduct
s. And the final one isValidData<T>
, meaning that its properties must have uniqueid
s.Okay, let’s test it:
Looks good. For
data1
everything passes. Fordata2
, the'VALUE1'
property fails, and theid
properties for bothnum1
andnum2
have 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