简体   繁体   中英

Narrow type based on conditional type(Typescript)

I want to create function type where one of the optional parameters are required.

As far as I get is to make conditional type but the problem is that in the function typescript can not narrow type based on this condition

type a = { propA: number };
type b = { propB: string };

type OneOfIsRequired = <T extends a | undefined, S extends b | undefined>
  (parameterOne: S extends undefined ? a : T, parameterTwo?: T extends undefined ? b : S, x: number) => any;

const fn: OneOfIsRequired = (a, b) => {
  if (a) {
    const propA = a.propA;
  } else {
    const propB = b.propB; // Object is possibly 'undefined'.. typescript can not narrow type based on first if statement
  }
};


fn(undefined, undefined, 1); // Argument of type 'undefined' is not assignable to parameter of type 'a' OK !, one of parameter is required
fn({ propA: 1 }, undefined, 1);
fn(undefined, { propB: '1' }, 1);

so i would expected that in lse condition in my function typescript can narrow correct type which is type "b" and not "b | undefined"

Any idea how i can achieve this behavior? I do not want to retype it by myself

I don't think conditional types are helping you much. I'd probably instead use a union of rest tuples to describe the possible arguments:

type OneOfIsRequired = (...args: [a, b | undefined, number] | [undefined, b, number]) => any;

This should give you the same results when you call it:

fn(undefined, undefined, 1); // error
fn({ propA: 1 }, undefined, 1); // okay
fn(undefined, { propB: '1' }, 1); // okay

But it has the benefit that the compiler is much more likely to be able to narrow a union to one of its consitutents than it is to be able to narrow a generic conditional type to one of its concrete values.


The implementation will still complain though, because TypeScript type guards will only narrow the type of a single value. That is, in if (a) { } else { } , it's possible that the type of a will be narrowed inside the then and else clauses, but the type of b will not be narrowed when you check a , even if there is some constraint between the types of a and b .

The only way to have a type guard happen automatically is to have a and b part of a single value and check that single value. You could make your own object like

const fn: OneOfIsRequired = (a, b, x) => {
  const obj = { a: a, b: b } as { a: a, b: b | undefined } | { a: undefined, b: b };
  if (obj.a) {
    const propA = obj.a.propA;
  } else {
    const propB = obj.b.propB; 
  }
};

but you already have an object kind of like this if you use the rest tuple in your implementation:

// use the arguments as a tuple
const fn: OneOfIsRequired = (...ab) => {
  if (ab[0]) {
    const propA = ab[0].propA;
  } else {
    const propB = ab[1].propB; 
  }
};

So this works but might be more refactoring than you want to do.


If all of this is too much work for you, just admit that you are smarter than the compiler and use a type assertion to tell it so. Specifically, you can use a non-null assertion using ! :

const fn: OneOfIsRequired = (a, b) => {
  if (a) {
    const propA = a.propA;
  } else {
    const propB = b!.propB; // I am smarter than the compiler 🤓
  }
};

That b! just means that you've told the compiler that b is not undefined , no matter what it thinks. And the error goes away. This is less type safe than the above solutions, but it is much simpler and doesn't change your emitted JavaScript.


Okay, hope that helps; good luck!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM