Reputation: 651
When writing an interface to a dict like store, I'd like to distinguish data model and item in store, which is id & model. I want to add constrain that model itself does not used field id
in their interface, but I don't know how to do that.
type Item<T> = T & {id: string}
// T is type of model
// Item<T> is type of objects in the store, use id as primary key
following function is simplified version, which is used to add new item in store
function add<T>(model: Item<T>): T {
const id = uuid();
const result = {...model, id};
store.push(result);
return result;
}
it's better to add some constrain on T
that T
does not have id
property, otherwise T
would be same as Item<T>
, meanwhile the id
in Item<T>
is not the same one in T
, which would lead to bugs.
As a summary, I need some type like this
type Item<T extends "The type which allow any type without property id: string"> = T & {id : string}
I've tried the following approaches:
type T0 = Exclude<any, {id: string}>; // any
type T1 = Exclude<{}, {id: string}>; // {}
type SafeModel<T extends Exclude<T, {id: string}>>; //circular constrain
None of them works.
I want some thing like
type Model // define like this
const v0: Model = {name: 'someName'} // ok
const v2: Model = {value: 123, id: 'someId'} //ok
const v1: Model = {name: 'someName', id: 'someId'} //error
or the way to bypass circular constrain to define type, which allows to define
type Model<T> = T extends { id: string } ? never: T;
type Item<T extends Model<T>> = T & { id: string }
Upvotes: 5
Views: 2126
Reputation: 25850
Use the never
type to tell TypeScript id
should not be there:
interface Unidentifiable extends Object {
id?: never;
}
It will allow only objects without a property called id
and the ones with id
set to undefined
.
Use it as a constraint on your parameter type:
type Item<T extends Unidentifiable> = T & { id: string };
Usage:
interface Foo {
age: number;
name: string;
}
interface Bar {
profession: string;
id: string;
}
type T1 = Item<Foo> // OK
type T2 = Item<Bar> // Error
Upvotes: 2
Reputation: 3134
Typescript mostly uses a Structured Typing strategy (as opposed to Nominal). It is easier to express what a structure should have rather than not have.
Here is a generic type to accomplish something like this:
type XorExtend<T extends {
[K in keyof T]: K extends keyof A
? A[K] extends T[K]
? never
: T[K]
: T[K]
}, A extends object,> = T & A
type a = XorExtend<{ id: string }, { a: 1 }> // okay
type b = XorExtend<{ id: string }, { id: 'asdf' }> // not okay!
type c = XorExtend<{ id: string }, { id: 1 }> // okay
playground here
Presuming you're using a function somewhere you could leverage something this as well:
declare function xorExtend<O, P extends { [K in keyof P]: K extends keyof O
? O[K] extends P[K]
? never
: P[K]
: P[K]
}>(a: O, b: P): P & O
const test = xorExtend( { id: '' }, { b: '' } ) // ok, test: {id: ''} & {b: ''}
const test2 = xorExtend( { id: '' }, { id: 'whatever' } ) // not okay
const test3 = xorExtend( { id: '' }, { id: 4 } ) // okay
Link to playground here
Hope this helps!
Here is my gist for this on github:
https://gist.github.com/babakness/82eba31701195d9b68d4119a5c59fd35
Upvotes: 0