Reputation: 591
I'm trying to model a particular pattern with the type system where an object consists of exactly a single property of a set of possible properties.
In other words, the type would be a partial type but with only one property allowed.
interface PossibleProperties {
cat?: AllPropsOfSameType;
dog?: AllPropsOfSameType;
cow?: AllPropsOfSameType;
}
interface ShouldBeExactlyOneOfPossibleProperties {
[P in keyof PossibleProperties]: AllPropsOfSameType; // Wupz, this allows for 0 or more...
}
I've seen solutions for requiring at least one property:
type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]
But I need something like AtMostOne<T, U = {[K in keyof T]: Pick<T, K> }>
or ExactlyOne<T, U = {[K in keyof T]: Pick<T, K> }>
which could possibly be the intersection type of AtMostOne
and AtLeastOne
Any ideas if this is possible?
Upvotes: 12
Views: 10280
Reputation: 1674
Using information throughout this whole post, I get the following as a solution:
type Explode<T> = keyof T extends infer K
? K extends unknown
? { [I in keyof T]: I extends K ? T[I] : never }
: never
: never;
type AtMostOne<T> = Explode<Partial<T>>;
type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]
type ExactlyOne<T> = AtMostOne<T> & AtLeastOne<T>
Upvotes: 10
Reputation: 4648
I had some trouble getting @sebastian ferreira's answer to work, so I though I would post my solution here as well:
interface Foo {
a: number;
b: string;
c: boolean;
}
type AtMostOneOf<T> = keyof T extends infer K
? K extends unknown
? { [I in keyof T]+?: I extends K ? T[I] : never } // notice the '+?'
: never
: never;
type Test = AtMostOneOf<Foo>;
const test: Test = {a: 1}; // allowed
const test1: Test = {b: 'asd'}; // allowed
const test2: Test = {a: 1, b: 'asd'}; // not allowed
const test3: Test = {} // allowed
Upvotes: 0
Reputation: 3900
This solution uses types instead of an interface, but I think it does what you need.
First, define a base type which defines all the properties you want to support. They need to be optional and have a type of never:
type AnimalBaseType = {
"cat"?: never;
"dog"?: never;
"cow"?: never;
}
Now, define a type for each animal, based on AnimalBaseType, but omit the corresponding "never" property and replace it with the property you want.
type CatType = Omit<AnimalBaseType, "cat"> & { "cat": string };
type DogType = Omit<AnimalBaseType, "dog"> & { "dog": string };
type CowType = Omit<AnimalBaseType, "cow"> & { "cow": string };
You now have three types, each of which only allows the one property. These can be combined into one type and exported to the rest of your project:
export type AnimalType = CatType | DogType | CowType;
And here are some tests you can try:
// A dog can *not* be a cat
let dogCat : CatType = { dog: "woof" }
// A cat can *not* be a dog
let catDog : DogType = { cat: "meow" }
// An animal *can* be a either cat, dog or cow
let cat : AnimalType = { cat: "meow" }
let dog : AnimalType = { dog: "woof" }
let cow : AnimalType = { cow: "moo" }
// You can *not* specify more than one animal
let zoo : AnimalType = { cat: "meow", dog: "woof", cow: "moo" }
You can see the result here: Playground
Upvotes: 2
Reputation: 11
I think this could be a possible solution:
interface PossibleProperties {
cat?: any;
dog?: any;
cow?: any;
}
type keys = keyof PossibleProperties;
type Diff<S, T> = S extends T ? never : S;
type Container<T extends keys> = {
[k in T]?: never
}
type Single<R extends keys> = {
[k in R]: PossibleProperties[k]
}
type All<R extends keys, T extends keys> = Single<R> & Container<T>
type ExactOne<T> = T extends keys ? All<T, Diff<keys, T >> : never;
let d: ExactOne<keys> = {dog: 1}; //ok
let d2: ExactOne<keys> = {cat: 1}; //ok
let d3: ExactOne<keys> = {cow: 1}; //ok
let d4: ExactOne<keys> = {cow: 1, cat: 1}; //error
let d5: ExactOne<keys> = {}; //error
let d6: ExactOne<keys> = {huhu: true}; //error
Upvotes: 0
Reputation: 591
Answering my own question, but the credit is due to https://github.com/keithlayne as he came up with the solution on https://gitter.im/Microsoft/TypeScript
Credit is also due to https://github.com/fatcerberus who came up with the initial reasoning.
I’m pretty sure there’s no way to model that in TS, simply because of how structural typing works. You can always have extra properties that the type doesn’t explicitly list
Even if you make a union of single-property types, it’s no guarantee that you don’t have any of the others
Hmm... I might be wrong. If you could make a type that maps over the keys and makes a union like
{a:string,b:never} | {a:never,b:string}
then that could work. Not sure how you’d do it though
Here goes Keith's answer:
type Explode<T> = keyof T extends infer K
? K extends unknown
? { [I in keyof T]: I extends K ? T[I] : never }
: never
: never;
This is probably easier to grok:
type Split<T, K extends keyof T> = K extends unknown ? { [I in keyof T]: I extends K ? T[I] : never } : never;
type Explode<T> = Split<T, keyof T>;
Basically, map over T
for each member of keyof T
, and return the union.
Upvotes: 2
Reputation: 40722
The first my thought was to create a union type, like:
type ExactlyOne =
{ cat?: AllPropsOfSameType } |
{ dog?: AllPropsOfSameType } |
{ cow?: AllPropsOfSameType };
It's possible using distributive conditional types:
type ExactlyOne<T, TKey = keyof T> = TKey extends keyof T ? { [key in TKey]: T[TKey] } : never;
type ShouldBeExactlyOneOfPossibleProperties = ExactlyOne<PossibleProperties>;
But it still allows to assign an object with multiple properties:
// this assignment gives no errors
const animal: ShouldBeExactlyOneOfPossibleProperties = {
cat: 'a big cat',
dog: 'a small dog'
};
It's because union types in TypeScript are inclusive and you cannot create an exclusive union type at the moment. See this answer.
So we need to forbid additional properties somehow. An option might be to use never
type, but unfortunately it's impossible to create an optional property of type never
because never | undefined
gives undefined
. If it's ok to have undefined
additional properties you can use the following monstrous type:
type ExactlyOne<T, TKey = keyof T> = TKey extends keyof T
? { [key in Exclude<keyof T, TKey>]?: never } & { [key in TKey]: T[key] }
: never;
And the resulted type looks like:
({
dog?: undefined;
cow?: undefined;
} & {
cat: string | undefined;
}) | ({
cat?: undefined;
cow?: undefined;
} & {
dog: string | undefined;
}) | ({
cat?: undefined;
dog?: undefined;
} & {
cow: string | undefined;
})
It's horrible... but It's close to what is expected.
A disadvantage of this approach is an undescriptive error message if you try to assign an object with multiple properties, for example this assignment:
const animal: ShouldBeExactlyOneOfPossibleProperties = {
cat: 'a big cat',
dog: 'a small dog'
};
gives the following error:
Type '{ cat: string; dog: string; }' is not assignable to type '({ dog?: undefined; cow?: undefined; } & { cat: string | undefined; }) | ({ cat?: undefined; cow?: undefined; } & { dog: string | undefined; }) | ({ cat?: undefined; dog?: undefined; } & { cow: string | undefined; })'.
Type '{ cat: string; dog: string; }' is not assignable to type '{ cat?: undefined; dog?: undefined; } & { cow: string | undefined; }'.
Type '{ cat: string; dog: string; }' is not assignable to type '{ cat?: undefined; dog?: undefined; }'.
Types of property 'cat' are incompatible.
Type 'string' is not assignable to type 'undefined'.(2322)
Another approach: you can emulate an exclusive union type like suggested in this answer. But in this case an extra property is added to the object.
type ExactlyOne<T, TKey = keyof T> = TKey extends keyof T
? { [key in TKey]: T[TKey] } & { prop: TKey }
: never;
const animal: ExactlyOne<PossibleProperties> = {
prop: 'cat',
cat: 'a big cat'
};
Upvotes: 1