Reputation: 224
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}) {}
Upvotes: 4
Views: 1787
Reputation: 25790
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.
Upvotes: 6
Reputation: 15589
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
}
Upvotes: 1
Reputation: 12052
The quick and dirty solution would be to just say I know this is the correct type and just cast it to the target interface.
const middle : MiddleNamePerson = person as MiddleNamePerson;
The recommended solution is to use a Type Guard. Then the type will be automatically recognized as the narrowed type after using it. Here is the code using a type guard:
export interface Person {
midddle: string | null;
}
export interface MiddleNamePerson {
midddle: string;
}
// type guard function
function isMiddleNamePerson(person: Person): person is MiddleNamePerson {
return person != null && person.midddle != null;
}
export function DoWork(person: Person) {
if (isMiddleNamePerson(person)) {
DoStuff(person); // person is automaticaly recognized as MiddleNamePerson becuse of the type guard
}
}
function DoStuff(value: {midddle: string}) { }
Upvotes: 0