This example uses type system to staticaly validate whether there are duplicates or not.
interface IProduct<Id extends number> {
id: Id
name: string;
}
const product = <Id extends number>(id: Id, name: string) => ({ id, name })
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'] }]>
: never)
// #5 [id] is not a duplicate, hence we can add to our accumulator whole product
: (Tail extends IProduct<number>[]
? Validation<Tail, [...Accumulator, Head]>
: never)
)
: never)
: never)
)
type Ok = Validation<[{ id: 1, name: '1' }, { id: 2, name: '2' }]>
type Fail = Validation<[{ id: 1, name: '1' }, { id: 1, name: '2' }]> // id:never
const builder = <
Product extends IProduct<number>,
Products extends Product[]
>(...products: [...Products] & Validation<Products>) => products
builder(product(1, 'John'), product(2, 'Doe'))
Playground
Validation - iterates recursively through all passed into function products. If product[id] already exists in accumulator type - replace id property with never, otherwise just add product to accumulator.
Please see the comments #1, #2 ....
If you dont want to use rest operator, consider this example:
interface IProduct<Id extends number> {
id: Id
name: string;
}
const product = <Id extends number>(id: Id, name: string) => ({ id, name })
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)
)
type Ok = Validation<[{ id: 1, name: '1' }, { id: 2, name: '2' }]>
type Fail = Validation<[{ id: 1, name: '1' }, { id: 1, name: '2' }]> // id:never
const builder = <
Id extends number,
Product extends IProduct<Id>,
Products extends Product[]
>(products: Validation<[...Products]>) => products
builder([product(1, 'John'), product(1, 'John')]) // error
Playground
If you are interested in static validation, you can check my article