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}) {}
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.
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With