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 ValidData
in TypeScript corresponding to your requirement that each property have unique id
subproperties, so you can't write const data = {⋯} as const satisfies ValidData
. Instead you can make a generic ValidData<T>
type that checks the input type T
and validates it, along with a helper validData()
function. So you'd write const 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 all ValidData<T>
types, where we don't care about the uniqueness of the id
; all we care about is that it has keys K
and values whose properties are all Product
s:
type BasicData<K extends PropertyKey = EnumValue> =
Record<K, { [k: string]: Product }>;
Note that I've used a default generic type argument so that BasicData
by itself corresponds to BasicData<EnumValue>
, which is basically the same as what you were using after satisfies
.
Then, ValidData<T>
would look like
type ValidData<T extends BasicData<keyof T>> =
{ [K in keyof T]: UniqueId<T[K]> }
where UniqueId<T>
is a validator generic that makes sure T
has unique id
properties. Here's one way to write that:
type UniqueId<T extends Record<keyof T, Product>> =
{ [K in keyof T]: {
id: Exclude<T[K]["id"], T[Exclude<keyof T, K>]["id"]>,
name: string
} }
This is a mapped type over T
where each property is a Product
whose id
property explicitly Exclude
s the id
properties from all other keys. T[Exclude<keyof T, K>]
is the union of all properties of T
except the one with key K
. And so that id
property looks like "Take the id
property of this property and Exclude
the id
properties from all other properties."
If the id
properties are unique, this will end up not excluding anything. If they are not, then the id
property for the duplicates will end up being the never
type. So if UniqueId<T>
extends T
, then T
is valid. Otherwise, T
is invalid in exactly those properties with duplicate id
s.
So now we can write validData()
like this:
const validData =
<const T extends BasicData & BasicData<keyof T> & ValidData<T>>(
d: T) => d;
This uses a const
type parameter so that callers don't need to remember to use a const
assertion. The constraint is the intersection of all the individual constraints we care about. The first is BasicData
, meaning it must have all the keys of EnumValue
. The second is BasicData<keyof T>
, meaning for each property it does have, all of the properties must be a bag of Product
s. And the final one is ValidData<T>
, meaning that its properties must have unique id
s.
Okay, let's test it:
const data = validData({
'VALUE1': {
num1: { id: 1, name: '2' },
num2: { id: 2, name: '2' },
},
'VALUE2': {
num1: { id: 1, name: '2' },
},
'VALUE3': {
num1: { id: 1, name: '2' },
},
}); // okay
const data2 = validData({
'VALUE1': {
num1: { id: 1, name: '2' }, // error
num2: { id: 1, name: '2' }, // error
},
'VALUE2': {
num1: { id: 1, name: '2' },
},
'VALUE3': {
num1: { id: 1, name: '2' },
},
});
Looks good. For data1
everything passes. For data2
, the 'VALUE1'
property fails, and the id
properties for both num1
and num2
have errors saying that they are not assignable to never
. 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:
type Repl<T, U> = [T] extends [never] ? U : T;
// https://github.com/microsoft/TypeScript/issues/23689 workaround
interface ErrMsg<T> { nope: never, msg: T }
type UniqueId<T extends Record<keyof T, Product>> =
{ [K in keyof T]: {
id: Repl<Exclude<T[K]["id"], T[Exclude<keyof T, K>]["id"]>,
ErrMsg<`conflicts with ${string &
{ [P in keyof T]:
T[K]["id"] & T[P]["id"] extends never ? never : Exclude<P, K>
}[keyof T]}`>>,
name: string
} }
const data2 = validData({
'VALUE1': {
num1: { id: 1, name: '2' }, // ErrMsg<"conflicts with num2">
num2: { id: 1, name: '2' }, // ErrMsg<"conflicts with num1">
},
'VALUE2': {
num1: { id: 1, name: '2' },
},
'VALUE3': {
num1: { id: 1, name: '2' },
},
});
But I consider that out of scope for this question and I won't digress further by explaining it here.