Closed
Description
Non-nullable types (#7140)
- One of the big things we've wanted to do with the type system.
- A little bit of work left on the PR itself.
- New compiler switch.
--strictNullChecks
.
- Introduces two new (accessible) types called
null
andundefined
.- Writing
T?
is the same as writingT | null | undefined
.
- Writing
Non-immediate assignments
-
JS problem: all variables, by default, have the value
undefined
:var x: number; // ... x = 1;
- That's still valid!
- For that reason, we do some basic assignment-before-use checking.
-
"Rudimentary" at this point.
-
"Perfect" support would require full data-flow analysis.
- Function hoisting makes this hard.
-
Even Flow doesn't do full data-flow analysis.
-
Though they do control-flow and "type"-flow analysis.
function (x: string | number) { if (typeof x === "string") { x = +x; } // Type of 'x' here is 'number'. }
-
Hasn't been a problem so far, but it might start to be a problem for us?
-
Type-flow analysis is in some sense a very general form of data-flow analysis, so...
-
We're going to keep it in mind, but it's not clear what kind of performance implications it would have.
-
Type guards
-
Standard checks become type guards.
// Compiled with --strictNullChecks declare function f(x: number): string; let x: number?; if (x) { f(x); // Ok, type of x is number here } else { f(x); // Error, type of x is number? here } let a = x != null ? f(x) : ""; // Type of a is string let b = x && f(x); // Type of b is string?
x
,x == null
,x == undefined
all have the same behavior.x === undefined
andx === null
have differing respective behavior.- What happens for teams that strictly use
null
orundefined
and use===
/!==
?- We actually wouldn't be using optionals within the compiler because we only use
undefined
! - We would make a
type Maybe<T> = T | undefined
. - Maybe we should add that and
type Nullable<T> = T | null
tolib.d.ts
.
- We actually wouldn't be using optionals within the compiler because we only use
-
Dotted names in type guards
interface Options { location?: { x?: number; y?: number; }; function foo(options?: Options) { if (options && options.location && options.location.x) { const x = options.location.x; // Type of x is number } }
- Works on optionals.
Arrays
- How do we deal with arrays and out-of-bounds?
- These are simply places we cannot solve the problem.
- The solution makes things insane to deal with.
- The cure is worse than the disease.
Behavior of widening and contextual typing
- Widening is no longer performed when using this new flag.
- Users now just get the
undefined
ornull
types when using these types.- So they won't be able to read from them.
- So they'll have to explicitly opt out.
- Does that mean we don't need
--noImplicitAny
?- Almost!
- Empty array literals are still an issue.
- So
--noImplicitAny
forces you to decide what to do with empty arrays.
- So
- Why would they not just become
undefined[]
?var a: number[] = [];
- How does this interact with the plan to "contextually type"
undefined
/null
?- We were saying this was necessary for contextually implementing class properties.
- Probably shouldn't be impacted much by this.
Non-null assertion operator
-
A new
!
postfix expression-level operator. -
Produces no runtime-dependent code.
// Compiled with --strictNullChecks function validateEntity(e: Entity?) { // Throw exception if e is null or invalid entity } function processEntity(e: Entity?) { validateEntity(e); let s = e!.name; // Assert that e is non-null and access name }
- Why can't a type predicate take care of that?
- It could but it's just cumbersome for some scenarios.
Optionals
-
Optional properties and parameters have fairly intuitive behavior.
function foo(x?: number) { }
- That implicitly gives
x
the typenumber | undefined
.
- That implicitly gives
-
What about default parameters?
function foo(x = 15) { }
- Externally,
x
isnumber | undefined
.- Declaration files will reflect this.
- Internally,
x
has typenumber
.- It's as if you had implicitly done the checking you needed (e.g.
if (x === undefined) { x = 15; }
). - Same work has been done for destructuring defaults as well.
- It's as if you had implicitly done the checking you needed (e.g.
- Externally,
Best common type issues
function f(x: boolean) {
if (x) {
return 100;
}
return undefined;
}
- There's no best common type between
undefined
andnumber
, so this caused an error. - This is a special case we need to start considering.
- Enum member types may need this if we choose to adopt them.
Adoption issues
- Non-null checks are an all-or-nothing sort of feature.
- Tons of declaration files have been written without this feature in mind (because, well, they couldn't).
- Becomes a game of wack-a-mole for consumers in patching up declaration files.
- We could consider a file-specific understanding, or use a tri-state of nullability, but this actually becomes a mental burden for both users and ourselves.
- Why does this get a switch but not
readonly
?- Meaningful work we can do with
readonly
that's backwards compatible.- Old code doesn't change semantics.
- Nullability doesn't really have a good story for backwards compatibility - you're now changing the semantics of the old syntax.
- Meaningful work we can do with