Home > Enterprise >  Can I enforce property-type uniqueness?
Can I enforce property-type uniqueness?

Time:01-26

I want to guarantee that there is not more than one field in T of type P. I am thinking something like:

declare function f<T, U extends Unique<T, string>>(t: T);

so that

f({a : '', b: 0, c: 0})

compiles but

f({a : '', b: 0, c: 0, d: ''})

does not.

I was thinking something from

type R = (keyof { a : string } ) ['length']

But R here is number not literally 1.

Edit: I need to be able to specify the type P, such that there is a maximum of one field of that type or subtype.

CodePudding user response:

If you want Unique<T, V> to extend T if and only if at most one property of T is assignable to V, then I think the following formulation does that with a minimum of weird edge cases (although weird edge cases can be very weird indeed, so you should test any edge cases you care about):

type ProhibitProperty<T, V> = unknown extends { 
    [K in keyof T]: T[K] extends V ? unknown : never 
}[keyof T] ? never : unknown;

type Unique<T extends object, V> = { [K in keyof T]: 
  T[K] & (T[K] extends V ? ProhibitProperty<Omit<T, K>, V> : unknown) 
}

declare function f<T extends object>(t: Unique<T, string>): void;

The idea: Unique<T, V> will ensure that if any property at key K is assignable to V, no property of T other than the one with key K is also assignable to V. It does this by inspecting Omit<T, K> which is like T but with the K-keyed property removed. It intersects the property type T[K] with ProhibitProperty<Omit<T, K>, V>.

For ProhibitProperty<T, V>, if any of the properties of T are assignable to V, the output type will be the never type, TypeScript's bottom type which absorbs all intersections (i.e., A & never reduces to never for all A). On the other hand, if none of the properties of T are assignable to V, the output type will be the unknown type, TypeScript's top type which is absorbed into all intersections (i.e., A & unknown is A for all A) .

So when you intersect T[K] with ProhibitProperty<Omit<T, K>, V>, you will either get T[K], if no other property of T is assignable to V, or never, if any other property of T is assignable to V. Oh, and if T[K] is not assignable to V we just intersect it with unknown, since we essentially leave all non-V properties alone.

Let's make sure it works:

f({ a: '', b: 0, c: 1, d: 2 }); // okay

f({ a: '', b: 0, c: '' }); // error!
//  ~  <------>  ~
// string is not assignable to never

Looks good. The compiler accepts the object literal with one string value, and rejects the one with multiple string values. In the error case, it complains about both string properties, which is about the best we can do, since there's no principled way in TypeScript to choose a member of a union (although terrible hacky methods exist, see How to transform union type to tuple type for how to do this and why you shouldn't).

Playground link to code

  •  Tags:  
  • Related