"For example, I could write
someObject?.someProperty?.someMethod()which makes the error go away; but it ignores the fact that it is almost always an error if any of those null coalescing operators have effect."
If having a null value there is an exceptional situation, then your types should not allow that null. It sounds like your types are too permissive. If it's an error when the value is null, then it stands to reason it should be a type error when the value is null.
Dealing with nullish values is something that we all have to deal with, but it's really hard to generalize about since what to do in the null case is very dependent on how your app/code works.
In general, when I have properties that need be non-null most of the time, I declare them as such.
interface MyType {
someObject: {
someProperty: {
someMethod(): void
}
}
}
And then only make the fields optional when required for that circumstance:
function makeMyType(values: Partial<MyType>): MyType {
return { ...defaultMyType, ...values }
}
Sometimes it can't really be avoided easily, and in this cases I do end up doing a lot this:
if (!foo) throw new Error('really expected foo to be here!')
console.log(foo.bar)
Or in some cases just:
if (!foo) return
console.log(foo.bar)
Which is fine. If it ever really is nullish, then you get a nice error message about what went went wrong. Or, in the second case, you just bail from a function that would be invalid in the null case.
It's hard to advise more specifically without the details of your types. But in the end if:
"it seems like a non-negligble portion of my code is dealing with nullish-able variables"
then I would look at your types first to make sure you remove as much nullishness as you can.
Another option is to use a type predicate function to validate whole objects which you can then use without testing every single property everywhere:
interface Foo {
foo: string
bar: string
baz: string
}
function validateFoo(maybeFoo: Partial<Foo>): maybeFoo is Foo {
return !!maybeFoo.foo && !!maybeFoo.bar && !!maybeFoo.baz
}
const partialFoo: Partial<Foo> = { bar: 'a,b,c' }
if (validateFoo(partialFoo)) {
partialFoo.bar.split(',') // works
}
Playground
Answer from Alex Wayne on Stack Overflow"For example, I could write
someObject?.someProperty?.someMethod()which makes the error go away; but it ignores the fact that it is almost always an error if any of those null coalescing operators have effect."
If having a null value there is an exceptional situation, then your types should not allow that null. It sounds like your types are too permissive. If it's an error when the value is null, then it stands to reason it should be a type error when the value is null.
Dealing with nullish values is something that we all have to deal with, but it's really hard to generalize about since what to do in the null case is very dependent on how your app/code works.
In general, when I have properties that need be non-null most of the time, I declare them as such.
interface MyType {
someObject: {
someProperty: {
someMethod(): void
}
}
}
And then only make the fields optional when required for that circumstance:
function makeMyType(values: Partial<MyType>): MyType {
return { ...defaultMyType, ...values }
}
Sometimes it can't really be avoided easily, and in this cases I do end up doing a lot this:
if (!foo) throw new Error('really expected foo to be here!')
console.log(foo.bar)
Or in some cases just:
if (!foo) return
console.log(foo.bar)
Which is fine. If it ever really is nullish, then you get a nice error message about what went went wrong. Or, in the second case, you just bail from a function that would be invalid in the null case.
It's hard to advise more specifically without the details of your types. But in the end if:
"it seems like a non-negligble portion of my code is dealing with nullish-able variables"
then I would look at your types first to make sure you remove as much nullishness as you can.
Another option is to use a type predicate function to validate whole objects which you can then use without testing every single property everywhere:
interface Foo {
foo: string
bar: string
baz: string
}
function validateFoo(maybeFoo: Partial<Foo>): maybeFoo is Foo {
return !!maybeFoo.foo && !!maybeFoo.bar && !!maybeFoo.baz
}
const partialFoo: Partial<Foo> = { bar: 'a,b,c' }
if (validateFoo(partialFoo)) {
partialFoo.bar.split(',') // works
}
Playground
I marked Alex Wayne's answer, above, as the accepted answer. It makes a lot of useful points about pain I've inflicted on myself.
But I'd like to take a moment to extol the virtues of nullCast, which has worked out far better than I expected.
function nullCast<TYPE>(value: TYPE | null | undefined) : TYPE
{
if (!value) throw Error("Unexpected nullish value.");
return value;
}
The principal beautify of nullCast is that one never needs to supply the template parameter. TypeScript will correctly infer the non-nullish return type automatically. This works.
class Foo {
optionalBarValue?: Bar;
getBarValue() : Bar {
// no explicit template parameter required.
return nullCast(this.optionalBarValue);
}
};
where "correct" means an exception will be thrown at runtime if this.optionalBarValue is undefined, and the automatically inferred return type of nullCast() at compile time is Bar, with any combination of | null, and | undefined stripped away.
Unlike the null-assertion operator '!' which produces no runtime code at all, nullCast does generate code, and does throw definitively. Compare
let value: TYPE = this.objMember!.member!;
// no effect at runtime'. An error is throw if .objMember
// evaluates to null or undefined, but no exception is thrown
// if .member is nullish. The value variable ends up
// holding a value at runtime that is not of correct type.
let value: TYPE = nullCast(this.objMember?.member);
// Throws convincingly at runtime if either .objMember
// or .member evaluate to nullish. The inferred return type
// of .member is non-nullish concrete type of .member.
There's nothing terribly wrong with using if statements to remove nullishness in following code. But it is intrusive, and has a negative effect on compactness and legibility. If your intention is to throw an error when nullish assumptions are violated, nullCast can be used compactly and efficiently inline when evaluating expressions, while still maintaining clear visiblity for readability purposes. e.g.:
let value1 = nullCast(this.member1);
let value2 = nullCast(this.member2.function()?.member);
or even (somewhat hesitantly)
let value = nullCast(this.function1)(arg1,arg2);
Motivation: One still needs to evaluate on a case-by-case basis what the correct response to a nullish value is. But the brutal easiness of if (!value) return; makes it far too easy to do the wrong thing. If a nullish value violates an assumption in code, the correct response is to throw at runtime, rather than return without regard for the delayed consequences of not doing what should have been done. nullCast, in certain cases, makes it easier to do the right thing.
Anyway. Back to doing my homework on type predicates.
Videos
Since switching to TypeScript I have been using a lot of optional properties, for example:
type store = {
currentUserId?: string
}
function logout () {
store.currentUserId = undefined
}However my coworkers and I have been discussing whether null is a more appropriate type instead of undefined, like this:
type store = {
currentUserId: string | null
}
function logout () {
store.currentUserId = null
}It seems like the use of undefined in TypeScript differs slightly from in Javascript.
Do you guys/girls use undefined or null more often? And, which of the examples above do you think is better?
If I have a variable of type NonNullable<unknown> (equivalent to Object or {}), in other words, "any non-nullish value", why can I assign it to object (a.k.a "any object"), when they aren't the same thing?
Example:
const a: NonNullable<unknown> = 1; // Valid (makes sense) const b: object = 1; // Invalid (makes sense since 1 is not an object) const c: object = a; // Valid (doesn't make sense because `a` may not be an object as it's any non-nullish value)
This doesn't make any sense to me and seems to create runtime errors. For example, checking if an object has a given property.
Another issue I've seen which I'm not sure is related or not, is that type guards for nullish values on unknown values don't work as (I) expected, as in, it doesn't narrow the type to NonNullable<unknown>, even though NonNullable<unknown> | null | undefined should be equivalent to/the same as unknown.
Example:
const a: unknown = 1;
if (a === null || a === undefined) {
console.log(a);
} else {
console.log(a);
// ^? - Still gives unknown
}
const a: undefined | null | NonNullable<unknown> = 1;
if (a === null || a === undefined) {
console.log(a);
} else {
console.log(a);
// ^? - Correctly narrows to NonNullable<unknown>
}Is it this problem related to the above issue? Or are they two different problems and if so what's going on with each?
In my Express server, I have the situation that certain request properties are guaranteed by middleware, but the compiler doesn't know.
Hence, req.user could be "possibly undefined" even though I have middleware that checks for it before every request:
Lint doesn't like the ! operator to assert non-nullishness. But it doesn't complain when using as:
My question here: Are there any actual benefits of using the as operator here? Or is it effectively exactly the same?