如何创建一个局部样,需要一个单一的属性被设置

我们的结构如下:

export type LinkRestSource = {
model: string;
rel?: string;
title?: string;
} | {
model?: string;
rel: string;
title?: string;
} | {
model?: string;
rel?: string;
title: string;
};

也就是说

type LinkRestSource = Partial<{model: string, rel: string, title: string}>

除了这将允许传入一个空对象,而初始类型需要传入一个属性

我如何创建一个类似于 Partial的泛型,但是它的行为与上面的结构类似?

44643 次浏览

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);

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 Partial<T>3. 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 Partial<T>4 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 Partial<T>0 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!

There's another solution if you know which properties you want.

type 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.

type LinkRestSource = AtLeast<T, 'model' | 'rel'>

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', titel: 'c' } // incorrectly spelled property

and here's the TS playground link to try it

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]

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 }




In my case I wanted at least one property to be actually set (not just drawn from the union, in which some paths had undefined values by design).

The simplest formulation I could produce was...

type SomePropertyFrom<T> = { [K in keyof T]: Pick<Required<T>, K> }[keyof T]

I didn't find any of the terser approaches above to work, when dealing with a complex union like e.g. {concurrent:number} | {concurrent?:never} and the more long-winded ones looked terrifying and I'd rather understand my types fully.

My approach converged on a variant ofthe solution by gafi which was type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T] but where crucially I pick from Required<T>, else undefined remains in the set of valid property values coming from my type unions (and it therefore still doesn't exclude the empty object).

It should be possible to use the above in isolation, but for reference my full solution for defining a non-empty ScheduleOptions type, backed by a complex union, is shown below. This example also shows a definition for AllOrNothing<T>, which may be a complementary type for these kinds of problems...

/** Allows ConcurrencyLimit properties, or IntervalLimit properties or both, but requires at least one group to be fully set */
type ScheduleOptions = SomeLimit & {
errorHandler?: (err: unknown) => unknown;
};


/** A limit on the number of pending promises (created but not yet settled) */
interface ConcurrencyLimit {
concurrency: number;
}


/** A limit on the number of promises created within a millisecond interval */
interface IntervalLimit {
intervalCap: number;
intervalMs: number;
}


/** Allow any limit to be set or unset (implicitly includes case of no limits set, which we will exclude in the next step) */
type AnyLimit = AllOrNothing<ConcurrencyLimit> & AllOrNothing<IntervalLimit>;


/** Require at least some limit to be set (excludes case of no limits) */
type SomeLimit = AnyLimit & SomePropertyFrom<AnyLimit>;


/** Require a type's properties to be either fully present, or fully absent */
type AllOrNothing<T> =
| T
| {
[k in keyof Required<T>]?: never;
};


/** Require at least one assigned property from T */
type SomePropertyFrom<T> = { [K in keyof T]: Pick<Required<T>, K> }[keyof T];