Skip to content

Narrow falsy strings to '' and falsy numbers to 0 #45836

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22560,6 +22560,28 @@ namespace ts {
return filterType(type, t => (getTypeFacts(t) & include) !== 0);
}

function truthyTypeFilter(type: Type, truthiness: boolean): Type | undefined {
if (!truthiness) {
if (type.flags & TypeFlags.BigInt) {
return zeroBigIntType;
}
else if (type.flags & TypeFlags.Boolean) {
return falseType;
}
else if (type.flags & TypeFlags.String) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There probably needs to be a TemplateString and StringMapping case now, too, since both type kinds could possibly contain the empty string.

return emptyStringType;
}
else if (type.flags & TypeFlags.Number) {
return zeroType;
}
}

const facts = getTypeFacts(type);
const include = truthiness ? TypeFacts.Truthy : TypeFacts.Falsy;

return ((facts & include) === 0) ? undefined : type;
}

function getTypeWithDefault(type: Type, defaultExpression: Expression) {
return defaultExpression ?
getUnionType([getNonUndefinedType(type), getTypeOfExpression(defaultExpression)]) :
Expand Down Expand Up @@ -22790,10 +22812,10 @@ namespace ts {
return type.flags & TypeFlags.UnionOrIntersection ? every((type as UnionOrIntersectionType).types, f) : f(type);
}

function filterType(type: Type, f: (t: Type) => boolean): Type {
function filterAndNarrowType(type: Type, narrow: (t: Type) => Type | undefined) {
if (type.flags & TypeFlags.Union) {
const types = (type as UnionType).types;
const filtered = filter(types, f);
const filtered = compactMap(types, narrow);
if (filtered === types) {
return type;
}
Expand All @@ -22806,7 +22828,7 @@ namespace ts {
// Otherwise, if we have exactly one type left in the origin set, return that as the filtered type.
// Otherwise, construct a new filtered origin type.
const originTypes = (origin as UnionType).types;
const originFiltered = filter(originTypes, t => !!(t.flags & TypeFlags.Union) || f(t));
const originFiltered = compactMap(originTypes, t=> !!(t.flags & TypeFlags.Union) ? t : narrow(t));
if (originTypes.length - originFiltered.length === types.length - filtered.length) {
if (originFiltered.length === 1) {
return originFiltered[0];
Expand All @@ -22816,7 +22838,16 @@ namespace ts {
}
return getUnionTypeFromSortedList(filtered, (type as UnionType).objectFlags, /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, newOrigin);
}
return type.flags & TypeFlags.Never || f(type) ? type : neverType;

if (type.flags & TypeFlags.Never) {
return type;
}

return narrow(type) || neverType;
}

function filterType(type: Type, f: (t: Type) => boolean): Type {
return filterAndNarrowType(type, (t) => f(t) ? t : undefined);
}

function removeType(type: Type, targetType: Type) {
Expand Down Expand Up @@ -23756,7 +23787,7 @@ namespace ts {
function narrowTypeByTruthiness(type: Type, expr: Expression, assumeTrue: boolean): Type {
if (isMatchingReference(reference, expr)) {
return type.flags & TypeFlags.Unknown && assumeTrue ? nonNullUnknownType :
getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
filterAndNarrowType(type, t => truthyTypeFilter(t, assumeTrue));
}
if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) {
type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
Expand Down
26 changes: 26 additions & 0 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,32 @@ namespace ts {
return array;
}

/**
* Maps, filters for truthiness, and avoids allocation if all elements map to themselves.
*/
export function compactMap<T>(array: T[], f: (item: T) => T | undefined): T[] {
for (let i = 0; i < array.length; i++) {
const mapped = f(array[i]);
if (mapped && mapped === array[i]) {
continue;
}

const sliced = array.slice(0, i);
const compacted = mapped ? [...sliced, mapped] : sliced;

for (let j = i + 1; j < array.length; j++) {
const nextMapped = f(array[j]);
if (nextMapped) {
compacted.push(nextMapped);
}
}

return compacted;
}

return array;
}

/**
* Flattens an array containing a mix of array or non-array elements.
*
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/classStaticBlock8.types
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function foo (v: number) {
>4 : 4
}
switch (v) {
>v : number
>v : 0 | 1 | 3

default: break; // valid
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ let cond: boolean;
>0 : 0

x; // string | number
>x : string | number
>x : number | ""

x = "";
>x = "" : ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function d() {
>length : number

x; // number
>x : number
>x : 0
}
function e() {
>e : () => void
Expand Down
14 changes: 7 additions & 7 deletions tests/baselines/reference/controlFlowTruthiness.types
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function f1() {
}
else {
x; // string | undefined
>x : string | undefined
>x : "" | undefined
}
}

Expand All @@ -42,7 +42,7 @@ function f2() {
}
else {
x; // string | undefined
>x : string | undefined
>x : "" | undefined
}
}

Expand All @@ -63,7 +63,7 @@ function f3() {
}
else {
x; // string | undefined
>x : string | undefined
>x : "" | undefined
}
}

Expand All @@ -82,7 +82,7 @@ function f4() {
>foo : () => string | undefined

x; // string | undefined
>x : string | undefined
>x : "" | undefined
}
else {
x; // string
Expand Down Expand Up @@ -115,10 +115,10 @@ function f5() {
}
else {
x; // string | undefined
>x : string | undefined
>x : "" | undefined

y; // string | undefined
>y : string | undefined
>y : "" | undefined
}
}

Expand Down Expand Up @@ -153,7 +153,7 @@ function f6() {
>x : string | undefined

y; // string | undefined
>y : string | undefined
>y : "" | undefined
}
}

Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/expr.types
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ function f() {
n||n;
>n||n : number
>n : number
>n : number
>n : 0

n||e;
>n||e : number
Expand Down Expand Up @@ -238,7 +238,7 @@ function f() {
s||s;
>s||s : string
>s : string
>s : string
>s : ""

s||e;
>s||e : string | E
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ var rc3 = a3 || a3; // number || number is number
>rc3 : number
>a3 || a3 : number
>a3 : number
>a3 : number
>a3 : 0

var rc4 = a4 || a3; // string || number is string | number
>rc4 : string | number
Expand Down Expand Up @@ -237,7 +237,7 @@ var rd4 = a4 || a4; // string || string is string
>rd4 : string
>a4 || a4 : string
>a4 : string
>a4 : string
>a4 : ""

var rd5 = a5 || a4; // void || string is void | string
>rd5 : string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,42 +19,42 @@ function foo(x: number | string) {
>10 : 10
}
function foo2(x: number | string) {
>foo2 : (x: number | string) => number | true
>foo2 : (x: number | string) => true | 0 | 10
>x : string | number

// modify x in right hand operand
return typeof x !== "string" || ((x = 10) || x); // string | number
>typeof x !== "string" || ((x = 10) || x) : number | true
>typeof x !== "string" || ((x = 10) || x) : true | 0 | 10
>typeof x !== "string" : boolean
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : string | number
>"string" : "string"
>((x = 10) || x) : number
>(x = 10) || x : number
>((x = 10) || x) : 0 | 10
>(x = 10) || x : 0 | 10
>(x = 10) : 10
>x = 10 : 10
>x : string | number
>10 : 10
>x : number
>x : 0
}
function foo3(x: number | string) {
>foo3 : (x: number | string) => string | true
>foo3 : (x: number | string) => true | "" | "hello"
>x : string | number

// modify x in right hand operand with string type itself
return typeof x !== "string" || ((x = "hello") || x); // string | number
>typeof x !== "string" || ((x = "hello") || x) : string | true
>typeof x !== "string" || ((x = "hello") || x) : true | "" | "hello"
>typeof x !== "string" : boolean
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : string | number
>"string" : "string"
>((x = "hello") || x) : string
>(x = "hello") || x : string
>((x = "hello") || x) : "" | "hello"
>(x = "hello") || x : "" | "hello"
>(x = "hello") : "hello"
>x = "hello" : "hello"
>x : string | number
>"hello" : "hello"
>x : string
>x : ""
}
function foo4(x: number | string | boolean) {
>foo4 : (x: number | string | boolean) => boolean
Expand Down Expand Up @@ -103,7 +103,7 @@ function foo5(x: number | string | boolean) {
>typeof x === "number" // number | boolean || x : boolean
>typeof x === "number" : boolean
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : number | boolean
>x : 0 | boolean
>"number" : "number"

|| x)); // boolean
Expand Down Expand Up @@ -168,7 +168,7 @@ function foo7(x: number | string | boolean) {
>typeof x === "number" // change value of x ? ((x = 10) && x.toString()) // number | boolean | string // do not change value : ((y = x) && x.toString()) : string
>typeof x === "number" : boolean
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : number | boolean
>x : 0 | boolean
>"number" : "number"

// change value of x
Expand Down