TypeScript require one parameter or the other, but not neither

instanceof typescript
typescript either type
typescript dictionary type
typescript object key type
typescript conditional interface property
typescript keyof
typescript function type
typescript as

Say I have this type:

export interface Opts {
  paths?: string | Array<string>,
  path?: string | Array<string>
}

I want to tell the user that they must pass either paths or path, but it is not necessary to pass both. Right now the problem is that this compiles:

export const foo = (o: Opts) => {};
foo({});

does anyone know to allow for 2 or more optional but at least 1 is necessary parameters with TS?

You may use

export type Opts = { path: string | Array<string> } | { paths: string | Array<string> }

To increase readability you may write:

type StringOrArray = string | Array<string>;

type PathOpts  = { path : StringOrArray };
type PathsOpts = { paths: StringOrArray };

export type Opts = PathOpts | PathsOpts;

Advanced Types · TypeScript, An intersection type combines multiple types into one. This allows you to add together existing types to get a single type that has all the features you need. You will mostly see intersection types used for mixins and other concepts that don't fit in That means that we can call it with an argument that's neither a number nor a  Note that value can neither be a string nor a number within the last else branch. In that case, TypeScript infers the never type because we've annotated the value parameter to be of type string | number, that is, no other type than string or number is possible for the value parameter.

If you already have that interface defined and want to avoid duplicating the declarations, an option could be to create a conditional type that takes a type and returns a union with each type in the union containing one field (as well as a record of never values for any other fields to dissalow any extra fields to be specified)

export interface Opts {
    paths?: string | Array<string>,
    path?: string | Array<string>
}

type EitherField<T, TKey extends keyof T = keyof T> =
    TKey extends keyof T ? { [P in TKey]-?:T[TKey] } & Partial<Record<Exclude<keyof T, TKey>, never>>: never
export const foo = (o: EitherField<Opts>) => {};
foo({ path : '' });
foo({ paths: '' });
foo({ path : '', paths:'' }); // error
foo({}) // error

Edit

A few details on the type magic used here. We will use the distributive property of conditional types to in effect iterate over all keys of the T type. The distributive property needs an extra type parameter to work and we introduce TKey for this purpose but we also provide a default of all keys since we want to take all keys of type T.

So what we will do is actually take each key of the original type and create a new mapped type containing just that key. The result will be a union of all the mapped types that contain a single key. The mapped type will remove the optionality of the property (the -?, described here) and the property will be of the same type as the original property in T (T[TKey]).

The last part that needs explaining is Partial<Record<Exclude<keyof T, TKey>, never>>. Because of how excess property checks on object literals work we can specify any field of the union in an object key assigned to it. That is for a union such as { path: string | Array<string> } | { paths: string | Array<string> } we can assign this object literal { path: "", paths: ""} which is unfortunate. The solution is to require that if any other properties of T (other then TKey so we get Exclude<keyof T, TKey>) are present in the object literal for any given union member they should be of type never (so we get Record<Exclude<keyof T, TKey>, never>>). But we don't want to have to explicitly specify never for all members so that is why we Partial the previous record.

Functions · TypeScript, Captured variables are not reflected in the type. Required, optional, and default parameters all have one thing in common: they talk When passing arguments for a rest parameter, you can use as many as you want; you can even pass none. Methods, on the other hand, are only created once and attached to Handler's  Using Import = Require Syntax With TypeScript 2.2 In Angular 2.4.9 Using ANY Type Prevents Function Parameter Type-Checking In TypeScript 2.1.5 it's 6 of one

This works.

It accepts a generic type T, in your case a string.

The generic type OneOrMore defines either 1 of T or an array of T.

Your generic input object type Opts is either an object with either a key path of OneOrMore<T>, or a key paths of OneOrMore<T>. Although not really necessary, I made it explicit with that the only other option is never acceptable.

type OneOrMore<T> = T | T[];

export type Opts<T> = { path: OneOrMore<T> } | { paths: OneOrMore<T> } | never;

export const foo = (o: Opts<string>) => {};

foo({});

There is an error with {}

Conditional types in TypeScript, So we can either add yet another overload signature for the string | null Here we've introduced a type variable T for the text parameter. A 'bottom' type is one which no other types are assignable to, and And imagine that we needed to write a function that used only those animals which are also cats. In TypeScript, every parameter is assumed to be required by the function. This doesn’t mean that it can’t be given null or undefined, but rather, when the function is called, the compiler will check that the user has provided a value for each parameter.

You are basically looking for an exclusive union type.

It has been already proposed but unfortunately, in the end, it was declined.

I found the proposed solutions here not to my liking, mostly because I'm not a fan of fancy and complex types.

Have you tried with function overloading?

I was in a similar situation and for me, this was the solution.

interface Option1 {
  paths: string | string[];
}

interface Option2 {
  path: string | string[];
}

function foo(o: Option1): void;
function foo(o: Option2): void;
function foo(o: any): any {}

foo({ path: './' });
foo({ paths: '../' });
// The folling two lines gives an error: No overload matches this call.
foo({ paths: './', path: '../' });
foo({})

With arrow function the same code as above would instead be:

interface Option1 {
  paths: string | string[];
}

interface Option2 {
  path: string | string[];
}

interface fooOverload {
  (o: Option1): void;
  (o: Option2): void;
}

const foo: fooOverload = (o: any) => {};

foo({ path: '2' });
foo({ paths: '2' });
// The following two lines gives an error: No overload matches this call.
foo({ paths: '', path: 'so' });
foo({});

Hope this helps you out!

Conditional Types in TypeScript, A conditional type describes a type relationship test and selects one of two Neither string nor string[] are assignable to null | undefined , which is why the first Another useful feature that conditional types support is inferring type We need to pass a type as an argument for the type parameter T , not a  If the mapped type is not homomorphic you’ll have to give an explicit type parameter to your unwrapping function. Conditional Types. TypeScript 2.8 introduces conditional types which add the ability to express non-uniform type mappings. A conditional type selects one of two possible types based on a condition expressed as a type relationship

Type hole with compatibility between optional parameters/extra , No TypeScript error on the const z: (a: string) => number = y; line nor the z('x'). TypeScript does not guarantee soundness and this is one of the holes. If the callback needs parameters threaded through they would need to be mentioned On the other hand, the types are actually quite different and it is  Ok, I've had a bit of a play with this. Using tsc from the command line with --module system works as expected.. I then put some debug lines in transplier.js to see what's happening and it appears ts.createProgram is being called with the correct parameters, but it's generating code with require imports rather than systemjs imports.

TypeScript: Transforming optional properties to required properties , Here's a handy TypeScript generic for transforming a type with optional properties exist, but the optional properties on the original type may be undefined on… In either case, accessing the property foo may return the value undefined . by extracting just that one property extends (read: is assignable to) the same type  But this is not from a TypeScript module, so it doesn't use export default, nor from a module that tries to support TS, which would politely define exports.default. One possible cause of this is: you used import thing from "thing" and it compiles because allowSyntheticDefaultImports is true in tsconfig.json. That option affects compilation only, and doesn't create magic defaultiness in the emitted JS.

The Definitive TypeScript Guide, Unlike other compile-to-JavaScript languages, TypeScript does not try to that toNumber accepts one string parameter, and that it returns a number. Note that in many cases explicit type hints are not required (although it still may like to define types for a function that accepts either a number or a string. In TypeScript, optional and required parameters are interchangeable for the sake of function parameter type compatibility checks. Neither extra parameters of the source type nor optional

Comments
  • Could you please elaborate at why none of the provided answers are acceptable? What you would like included, what shortcomings you see with my approach and @hero-wanders. 10x
  • good idea, it might work, more verbose, maybe there is an even better way?
  • There is a very similar question and a quite powerful answer here: stackoverflow.com/a/48244432/10245948
  • Deriving from the solution I linked in my previous comment, this might be an alternative: If you define interface AllOpts { path: string | Array<string>, paths: string | Array<string> } and use the helper type OneOf<T, U = {[K in keyof T]: Pick<T, K> }> = U[keyof U] then you can write export type Opts = OneOf<AllOpts>;.
  • Just note that because of how excess property checking works on union, given your definition, this is also a valid call: foo({ path: "", paths: ""}) which I don't think the op wants.
  • If you want - deviating from your original question, exactly one, path or paths, to be present, use type PathOpts = { path: StringOrArray, paths: never }; and type PathsOpts = { paths: StringOrArray, path: never }; Thank you, Titian, for pointing this out.
  • @Buggy added an explanation, hope it's clear enough :)
  • Very good solution. I like how you realized that not both properties may be passed. Unfortunately the op explicitly said "at least 1 is necessary". If he actually wanted exactly one, your answer is the right way to handle this generically.
  • @TitianCernicova-Dragomir Hello Titian! How can we do this if we want an arg that is type {a:number, b:number} OR {x:number, y:number}, but not both, and not partial of either. For example, if I write {a:number, b:number} | {x:number, y:number} then it accepts a value like {a:1, b:2, y:3}, but I want only things like {a:1, b:2} or {x:1, y:2} without overlap.
  • @TitianCernicova-Dragomir Wow. Amazing.
  • Amazing! thanks @TitianCernicova-Dragomir