Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type Narrowing on interface Properties (removing null option)

Tags:

typescript

I have an interface that has a property that can be null. I want to check that's it's not null and then pass the object into a typesafe object

it appears that narrowing works when assigning the property to a variable but when try to use the object as a whole, it can't work out that it can't be null.

I don't want to use casting as that defeats the point of design time type checking.

interface Person
{
  midddle:string | null
}

interface MiddleNamePerson
{
  midddle:string
}

function DoWork(person:Person) {
  if(person.midddle)
  {
    const middleName:string = person.midddle; // works
    const middle : MiddleNamePerson = person // Error: Type of 'Person' not Assignable to 'MiddleNamePerson' 
    DoStuff(person) // Error: the argument of 'Person' is not Assignable to parameter
  }

}

function DoStuff(value:{midddle:string}) {}
like image 722
Tim Ker Avatar asked Oct 26 '25 04:10

Tim Ker


2 Answers

Solution

Replace this simple check:

if(person.midddle)

with a better type guard:

if(hasDefined(person, ['midddle']) {

Such a type guard can be defined as:

const hasDefined = <T, K extends keyof T>(argument: T | Defined<T, K>, keys: K[]): argument is Defined<T, K> =>
  keys.every(key => argument[key] != null)

type Defined<T, K extends keyof T = keyof T> = {
  [P in K]-?: Exclude<T[P], undefined | null>
}

Explanation

Control flow doesn't work upstream in TypeScript. By checking if(person.midddle) we know the middle name is truthy, but the definition for Person remains unaffected. It's still an object in which the property called middle can be null.

By changing the type guard in a way that it validates not a single field, but the entire object, we make sure person is a well-defined Person within the entire block of code.

like image 62
Karol Majewski Avatar answered Oct 28 '25 18:10

Karol Majewski


TypeScript currently doesn't do this kind of widening based on control flow analysis (as far as I know). It might be a good feature to add.

For now, while a bit involving, you can use typeguard.

function hasMidddle(person: Person): person is { midddle: string } {
  return !!person.midddle // btw your check will pass with empty string too.
}

function DoWork(person: Person) {
  if (hasMidddle(person)) {
    const middleName: string = person.midddle;
    const middle: MiddleNamePerson = person
    DoStuff(person)
  }
}

If you want to make the typeguard a bit nicer, you can use the ExcludePropType from type-plus:

function hasMidddle(person: Person): person is ExcludePropType<Person, null> {
  return !!person.midddle
}
like image 39
unional Avatar answered Oct 28 '25 17:10

unional



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!