Home > Net >  Clean way to narrow object property types in typescript
Clean way to narrow object property types in typescript

Time:12-31

In the below code, it is difficult to understand to me just why Typescript is able to properly narrow down the type of x when accessed separately but not on the object as a whole. Since o.x is "the property x on the object o", it seems incongruent that the type of "o.x" and the type of x as seen on o could possibly be different.

function checkPropType(o: {x: number | undefined, s: string}) {
  o.x ??= 5;

  takeNumber(o.x); // Great!

  takeOWithDefinedX(o) // T_T
  takeOWithDefinedX({...o, x: o.x}); // Works but ugly and wasteful.
}

function checkPropType2(o: {x: number | undefined, s: string}) {
  let {x} = o;
  x ??= 5;

  takeNumber(x); 

  takeOWithDefinedX(o) // T_T
  takeOWithDefinedX({...o, x}) // Works, but the destructuring makes it even more verbose overall
}

function takeNumber(x: number) {}
function takeOWithDefinedX(o: {x: number, s: string}) {}

https://tsplay.dev/NddKyN

Whatever the reason may be, it looks like we need to deal with it for now. I've shown two possible ways to do this, both of them involving creating an entire new object just to satisfy the TS compiler. The JS output shows it is not compiled away either. takeOWithDefinedX(Object.assign(Object.assign({}, o), { x: o.x }));

Is there a cleaner way to handle this issue?

CodePudding user response:

You could create a custom utility which takes a type and a prop as generic arguments, and it creates a mapped type which sets the type of the value of the prop to be that type excluding undefined. Then use it to assert that o is the type that you need. This emits no extra runtime code (check the JS output on the right in the playground link).

TS Playground

type RequiredProp<T, K extends keyof T> = T & Record<K, Exclude<T[K], undefined>>;

function takeNumber(x: number) {}
function takeOWithDefinedX(o: {x: number, s: string}) {}

function checkPropType(o: {x: number | undefined, s: string}) {
  o.x ??= 5;
  takeNumber(o.x);
  takeOWithDefinedX(o as RequiredProp<typeof o, 'x'>);
}

CodePudding user response:

I get your frustration, indeed you have an assign in your code to ensure that now your "x" is defined, but I don't think TS will be able to check all the code for you;

here's official, but a bit outdated answer

So you have 3 choices:

  1. Ignore TS

    if it's non-complex and rarely used types
    

    takeOWithDefinedX(o as {x: number; s:string})

  2. use @jsejcksn answer

    The same idea as "1",

    just a general hack, saying, "Yeah, yeah, - I'm sure I've checked all properties"

  3. Create conversion function

    type IAll = { x: number; s: string } // don't use "I" for the prefix, can't come up with something short )))
    type IOpt = { x?: number | undefined; s: string };
    
    function ensure(opt: IOpt, x = 5): IAll {
      let a: IAll = {x, s: opt.s};
      if (opt.x) a.x = opt.x;
      return a;
    }
    
    function checkPropType(o: IOpt) {
      takeOWithDefinedX(ensure(o)) // -_0
    }
    
    function takeNumber(x: number) {}
    function takeOWithDefinedX(o: IAll) {}
    
    
    

tsplay

  • Related