xiang0x48
xiang0x48

Reputation: 651

How to define a type in TypeScript which can have any properties except the specific one?

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

Answers (2)

Karol Majewski
Karol Majewski

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

Babakness
Babakness

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

Related Questions