Home > Software design >  Why TypeScript can't infer the type and still consider this property as null, after filtering f
Why TypeScript can't infer the type and still consider this property as null, after filtering f

Time:12-17

I'm checking with different TypeScript version. TypeScript surprise me a lot of time on how much is smart, but this...?

type Product = { id: number, imageUrl: null | string };
const products: Product[] = [
  { id: 1, imageUrl: 'assets/img/1.jpg' },
  { id: 2, imageUrl: null }
];

products.filter(p => null !== p.imageUrl).forEach(p => {
  p.imageUrl; // null | string
});

Am I'm doing something wrong or it's just a limitation of the language?

CodePudding user response:

It's a limitation of the language, but a simple common solution is to use the is syntax to convey this information downstream

products
  .map(
    (p: Product) => p.imageUrl
  )
  .filter(
    (imageUrl: string | null): imageUrl is string => !!imageUrl
  )
  .forEach(
    (imageUrl: string) => {
      imageUrl; // string
    }
  );

TypeScript Playground

CodePudding user response:

It is currently a limitation of the language. There are however some open issues in GitHub if you want to follow them: #38390, #16069 You can add a type guard and it should work (as suggested in the referenced issues):

type Product = { id: number, imageUrl: null | string };
const products: Product[] = [
  { id: 1, imageUrl: 'assets/img/1.jpg' },
  { id: 2, imageUrl: null }
];

products.filter((p): p is {id: number, imageUrl: string } => null !== p.imageUrl).forEach(p => {
  p.imageUrl; // string
});

CodePudding user response:

This is one of those times you have to work with the language:

type Product = { id: number, imageUrl: null | string };
const products: Product[] = [
  { id: 1, imageUrl: 'assets/img/1.jpg' },
  { id: 2, imageUrl: null }
];

products.forEach(p => {
  if (p.imageUrl) {
    // here the typechecker knows it's not null
  }
});

Note that this is also a more efficient iteration, although that might not matter in your case.

The problem with your original is that filter is a function from T[] => T[], not a function from T[] => SomeSubsetOfTTheCompilerKnowsYouNullChecked[].

The compiler can tell in a given scope/block if an access has been checked, and can even do this (in more recent versions) if there's an error path:

function f(x: null | string) {
  if (x === null) {
    throw new Error('oops');
  }

  x.repeat(3); // safe!
}

...but it can't do this across function calls:

function g(x: null | string) {
  return x;
}

const maybeString = Math.round(Math.random()) ? 'hi' : null;
if (maybeString) {
    const a = g(maybeString); // type is still null | string!
}

Playground

  • Related