Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RequireOnlyOne type #24

Closed
wants to merge 17 commits into from
33 changes: 33 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,36 @@ export type RequireAtLeastOne<ObjectType, KeysType extends keyof ObjectType = ke
}[KeysType]
// …then, make intersection types by adding the remaining properties to each mapped type.
& Omit<ObjectType, KeysType>;

/*
Create a type that requires at only one of the given properties. The remaining properties are kept as is.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Create a type that requires at only one of the given properties. The remaining properties are kept as is.
Create a type that requires only one of the given properties, but not more. The remaining properties are kept as is.

?


Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you mention in text some real-world use-cases?

Write about some real-world use-cases where it can be useful. (It can be hard sometimes for users to see where they would use something) - https://github.com/sindresorhus/type-fest/blob/master/.github/contributing.md

@example
```
import {RequireOnlyOne} from 'type-fest';

type Responder = {
text: () => string;
json: () => string;

secure: boolean;
};

const responder: RequireOnlyOne<Responder, 'text' | 'json'> = {
json: () => '{"message": "ok"}',
secure: true
};
```
*/
export type RequireOnlyOne<ObjectType, KeysType extends keyof ObjectType = keyof ObjectType> =
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Require only one" makes me think that it requires only one, but doesn't make it clear that it disallows more than one. Maybe we should simply name it RequireOne?

{
// For each Key in KeysType make a mapped type
[Key in KeysType]: (
// …by picking that Key's type and making it required
Required<Pick<ObjectType, Key>> &
// …and adding the other keys in KeysType as optional and of type `never`
Partial<Record<Exclude<KeysType, Key>, never>>
)
}[KeysType]
// …then, make intersection types by adding the remaining properties to each mapped type.
& Omit<ObjectType, KeysType>;
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ See the [types file](index.d.ts) for complete docs.
- `Merge` - Merge two types into a new type. Keys of the second type overrides keys of the first type.
- `MergeExclusive` - Create a type that has mutually exclusive properties.
- `RequireAtLeastOne` - Create a type that requires at least one of the given properties.
- `RequireOnlyOne` - Create a type that requires only one of the given properties.
- `LiteralUnion` - Allows creating a union type by combining primitive types and literal types without sacrificing auto-completion in IDEs for the literal type part of the union. Workaround for [Microsoft/TypeScript#29729](https://github.com/Microsoft/TypeScript/issues/29729).

### Miscellaneous
Expand Down
38 changes: 38 additions & 0 deletions test-d/require-only-one.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {expectType, expectError} from 'tsd';
import {RequireOnlyOne} from '..';

type SystemMessages = {
default: string;

macos?: string;
linux: string;
windows: string;

optional?: string;
};

type ValidMessages = RequireOnlyOne<SystemMessages, 'macos' | 'linux' | 'windows'>;
const test = (_: ValidMessages): void => {};

test({macos: 'hey', default: 'hello'});
test({linux: 'sup', default: 'hello'});
test({windows: 'hi', default: 'hello'});
test({windows: 'hi', default: 'hello', optional: 'howdy'});

const system = Math.random() > 0.5 ? {macos: 'hey'} : {linux: 'sup'};
test({default: 'hello', ...system});

expectError(test({}));
expectError(test({macos: 'hey'}));
expectError(test({default: 'hello'}));
expectError(test({macos: 'hey', linux: 'sup', default: 'hello'}));
expectError(test({macos: 'hey', default: 'hello', unknown: 'hmmm'}));

// TODO: Should it eventually be possible to make this negative test pass?
/*
const invalidSystem = Math.random() > 0.5 ? {default: 'hello', macos: 'hey', a: 1, b: 3} : {default: 'hello', linux: 'sup', b: 2};
expectError(test(invalidSystem));
*/

declare const onlyOneWithoutKeys: RequireOnlyOne<{a: number; b: number}>;
expectType<{a: number} | {b: number}>(onlyOneWithoutKeys);