How to create a Partial-like that requires a single property to be set
TypescriptGenericsTypescript Problem Overview
We have a structure that is like the following:
export type LinkRestSource = {
model: string;
rel?: string;
title?: string;
} | {
model?: string;
rel: string;
title?: string;
} | {
model?: string;
rel?: string;
title: string;
};
Which is almost the same as saying
type LinkRestSource = Partial<{model: string, rel: string, title: string}>
Except that this will allow an empty object to be passed in whereas the initial type requires one of the properties to be passed in
How can I create a generic like Partial
, but that behaves like my structure above?
Typescript Solutions
Solution 1 - Typescript
I think I have a solution for you. You're looking for something that takes a type T
and produces a related type which contains at least one property from T
. That is, it's like Partial<T>
but excludes the empty object.
If so, here it is:
type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]
To dissect it: first of all, AtLeastOne<T>
is Partial<T>
intersected with something. U[keyof U]
means that it's the union of all property values of U
. And I've defined (the default value of) U
to be a mapped type where each property of T
is mapped to Pick<T, K>
, a single-property type for the key K
. (For example, Pick<{foo: string, bar: number},'foo'>
is equivalent to {foo: string}
... it "picks" the 'foo'
property from the original type.) Meaning that U[keyof U]
in this case is the union of all possible single-property types from T
.
Hmm, that might be confusing. Let's see step-by-step how it operates on the following concrete type:
type FullLinkRestSource = {
model: string;
rel: string;
title: string;
}
type LinkRestSource = AtLeastOne<FullLinkRestSource>
That expands to
type LinkRestSource = AtLeastOne<FullLinkRestSource, {
[K in keyof FullLinkRestSource]: Pick<FullLinkRestSource, K>
}>
or
type LinkRestSource = AtLeastOne<FullLinkRestSource, {
model: Pick<FullLinkRestSource, 'model'>,
rel: Pick<FullLinkRestSource, 'rel'>,
title: Pick<FullLinkRestSource, 'title'>
}>
or
type LinkRestSource = AtLeastOne<FullLinkRestSource, {
model: {model: string},
rel: {rel: string},
title: {title: string}>
}>
or
type LinkRestSource = Partial<FullLinkRestSource> & {
model: {model: string},
rel: {rel: string},
title: {title: string}>
}[keyof {
model: {model: string},
rel: {rel: string},
title: {title: string}>
}]
or
type LinkRestSource = Partial<FullLinkRestSource> & {
model: {model: string},
rel: {rel: string},
title: {title: string}>
}['model' | 'rel' | 'title']
or
type LinkRestSource = Partial<FullLinkRestSource> &
({model: string} | {rel: string} | {title: string})
or
type LinkRestSource = {model?: string, rel?: string, title?: string} &
({model: string} | {rel: string} | {title: string})
or
type LinkRestSource = { model: string, rel?: string, title?: string }
| {model?: string, rel: string, title?: string}
| {model?: string, rel?: string, title: string}
which is, I think, what you want.
You can test it out:
const okay0: LinkRestSource = { model: 'a', rel: 'b', title: 'c' }
const okay1: LinkRestSource = { model: 'a', rel: 'b' }
const okay2: LinkRestSource = { model: 'a' }
const okay3: LinkRestSource = { rel: 'b' }
const okay4: LinkRestSource = { title: 'c' }
const error0: LinkRestSource = {} // missing property
const error1: LinkRestSource = { model: 'a', titel: 'c' } // excess property on string literal
So, does that work for you? Good luck!
Solution 2 - Typescript
There's another solution if you know which properties you want.
AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>
This would also allow you to lock in multiple keys of a type, e.g. AtLeast<T, 'model' | 'rel'>
.
Solution 3 - Typescript
A simpler version of the solution by jcalz:
type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T]
so the whole implementation becomes
type FullLinkRestSource = {
model: string;
rel: string;
title: string;
}
type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T]
type LinkRestSource = AtLeastOne<FullLinkRestSource>
const okay0: LinkRestSource = { model: 'a', rel: 'b', title: 'c' }
const okay1: LinkRestSource = { model: 'a', rel: 'b' }
const okay2: LinkRestSource = { model: 'a' }
const okay3: LinkRestSource = { rel: 'b' }
const okay4: LinkRestSource = { title: 'c' }
const error0: LinkRestSource = {} // missing property
const error1: LinkRestSource = { model: 'a', title: 'c' } // excess property on string literal
Solution 4 - Typescript
Maybe something like that:
type X<A, B, C> = (A & Partial<B> & Partial<C>) | (Partial<A> & B & Partial<C>) | (Partial<A> & Partial<B> & C);
type LinkRestSource = X<{ model: string }, { rel: string }, { title: string }>
var d: LinkRestSource = {rel: 'sdf'};
But it little bit messy :)
or
type Y<A, B, C> = Partial<A & B & C> & (A | B | C);
Solution 5 - Typescript
Unfortunately the above answers didn't work for me.
Either because the compiler couldn't catch the errors or because my IDE could not retrieve the expected attributes of an object even when it's type was annotated.
The following worked perfectly, and was taken from the official microsoft azure/keyvault-certificates package:
type RequireAtLeastOne<T> = { [K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>; }[keyof T]
Solution 6 - Typescript
Another way and if you need keep some properties required and at least one of rest required too. See Typescript Playground example.
The base interface could looks like:
export interface MainData {
name: string;
CRF: string;
email?: string;
cellphone?: string;
facebookId?: string;
}
...and if you only need at least one between 'email', 'cellphone' and 'facebookId', change and merge interfaces without optional symbol for every propoerty:
export interface registByEmail extends Omit<MainData, 'email'> { email: string }
export interface registByCellphone extends Omit<MainData, 'cellphone'> { cellphone: string }
export interface registByFacebook extends Omit<MainData, 'facebookId'> { facebookId: string }
export type RegistData = registByCellphone | registByEmail | registByFacebook
And results will looks like:
// language throws error
let client: RegistData = { name, CRF }
// its ok
let client: RegistData = { name, CRF, email }
let client: RegistData = { name, CRF, cellphone }
let client: RegistData = { name, CRF, facebookId }
let client: RegistData = { name, CRF, email, cellphone }