From 988e5a4f5eaad6b64857cf18942137b89824703b Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 5 Mar 2018 14:27:48 -0800 Subject: [PATCH 1/3] Use lib conditional types for type facts if possible This causes generics to be narrowed as one may expect --- src/compiler/checker.ts | 52 +++++++++ src/harness/fourslash.ts | 2 +- src/lib/es5.d.ts | 102 ++++++++++++++++++ .../strictGenericNarrowingUsesNonFalsy.js | 17 +++ ...strictGenericNarrowingUsesNonFalsy.symbols | 29 +++++ .../strictGenericNarrowingUsesNonFalsy.types | 30 ++++++ .../strictGenericNarrowingUsesNonFalsy.ts | 7 ++ 7 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.js create mode 100644 tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.symbols create mode 100644 tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.types create mode 100644 tests/cases/compiler/strictGenericNarrowingUsesNonFalsy.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e8eef1fe8d198..472d02dfcc849 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -402,6 +402,7 @@ namespace ts { let autoArrayType: Type; let anyReadonlyArrayType: Type; let deferredGlobalNonNullableTypeAlias: Symbol; + let deferredGlobalTypefactsNamespace: Symbol; // The library files are only loaded when the feature is used. // This allows users to just specify library files they want to used through --lib @@ -465,6 +466,29 @@ namespace ts { } } + const typeFactsKeysWithTypes = [ + ["EQString", TypeFacts.TypeofEQString], + ["EQNumber", TypeFacts.TypeofEQNumber], + ["EQBoolean", TypeFacts.TypeofEQBoolean], + ["EQSymbol", TypeFacts.TypeofEQSymbol], + ["EQObject", TypeFacts.TypeofEQObject], + ["EQFunction", TypeFacts.TypeofEQFunction], + ["NEString", TypeFacts.TypeofNEString], + ["NENumber", TypeFacts.TypeofNENumber], + ["NEBoolean", TypeFacts.TypeofNEBoolean], + ["NESymbol", TypeFacts.TypeofNESymbol], + ["NEObject", TypeFacts.TypeofNEObject], + ["NEFunction", TypeFacts.TypeofNEFunction], + ["EQUndefined", TypeFacts.EQUndefined], + ["EQNull", TypeFacts.EQNull], + ["EQUndefinedOrNull", TypeFacts.EQUndefinedOrNull], + ["NEUndefined", TypeFacts.NEUndefined], + ["NENull", TypeFacts.NENull], + ["NEUndefinedOrNull", TypeFacts.NEUndefinedOrNull], + ["Truthy", TypeFacts.Truthy], + ["Falsy", TypeFacts.Falsy], + ] as [__String, TypeFacts][]; + const enum TypeFacts { None = 0, TypeofEQString = 1 << 0, // typeof x === "string" @@ -12321,7 +12345,35 @@ namespace ts { return TypeFacts.All; } + function getGlobalTypeFactsNamespace() { + if (!deferredGlobalTypefactsNamespace) { + deferredGlobalTypefactsNamespace = getGlobalSymbol("TypeFacts" as __String, SymbolFlags.Namespace, /*diagnostic*/ undefined) || unknownSymbol; + } + return deferredGlobalTypefactsNamespace; + } + + function getFactType(ns: Symbol, factName: __String, type: Type): Type { + const alias = getExportOfModule(ns, factName, /*dontResolveAlias*/ false) || unknownSymbol; + if (!(alias.flags & SymbolFlags.TypeAlias)) { + return type; // Either invalid type or unknown symbol + } + return getTypeAliasInstantiation(alias, [type]); + } + function getTypeWithFacts(type: Type, include: TypeFacts) { + const ns = getGlobalTypeFactsNamespace(); + if (ns === unknownSymbol) { + return filterTypeWithFacts(type, include); + } + for (const [name, flag] of typeFactsKeysWithTypes) { + if (include & flag) { + type = getFactType(ns, name, type); + } + } + return type; + } + + function filterTypeWithFacts(type: Type, include: TypeFacts) { return filterType(type, t => (getTypeFacts(t) & include) !== 0); } diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 401caac323411..f253860a32d75 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -3229,7 +3229,7 @@ Actual: ${stringify(fullActual)}`); return ts.createTextSpanFromRange(ranges[index]); } else { - this.raiseError("Supplied span index: " + index + " does not exist in range list of size: " + (ranges ? 0 : ranges.length)); + this.raiseError("Supplied span index: " + index + " does not exist in range list of size: " + (!ranges ? 0 : ranges.length)); } } diff --git a/src/lib/es5.d.ts b/src/lib/es5.d.ts index 1f352cd6f3913..956cc4b35598f 100644 --- a/src/lib/es5.d.ts +++ b/src/lib/es5.d.ts @@ -1360,6 +1360,108 @@ type Extract = T extends U ? T : never; */ type NonNullable = T extends null | undefined ? never : T; +declare namespace TypeFacts { + /** + * Include only string from T + */ + export type EQString = T extends string ? T : never; + + /** + * Include only number from T + */ + export type EQNumber = T extends number ? T : never; + + /** + * Include only boolean from T + */ + export type EQBoolean = T extends boolean ? T : never; + + /** + * Include only symbol from T + */ + export type EQSymbol = T extends symbol ? T : never; + + /** + * Include only object from T + */ + export type EQObject = T extends object ? T : never; + + /** + * Include only functions from T + */ + export type EQFunction = T extends (...args: any[]) => any ? T : never; + + /** + * Exclude only string from T + */ + export type NEString = T extends string ? never : T; + + /** + * Exclude only number from T + */ + export type NENumber = T extends number ? never : T; + + /** + * Exclude only boolean from T + */ + export type NEBoolean = T extends boolean ? never : T; + + /** + * Exclude only symbol from T + */ + export type NESymbol = T extends symbol ? never : T; + + /** + * Exclude only object from T + */ + export type NEObject = T extends object ? never : T; + + /** + * Exclude only functions from T + */ + export type NEFunction = T extends (...args: any[]) => any ? never : T; + + /** + * Include only undefined from T + */ + export type EQUndefined = T extends undefined | void ? T : never; + + /** + * Include only null from T + */ + export type EQNull = T extends null ? T : never; + + /** + * Include only null and undefined from T + */ + export type EQUndefinedOrNull = T extends null | undefined | void ? T : never; + + /** + * Exclude only undefined from T + */ + export type NEUndefined = T extends undefined | void ? never : T; + + /** + * Exclude only null from T + */ + export type NENull = T extends null ? never : T; + + /** + * Exclude null and undefined from T (different from NonNullable as it also filters void) + */ + export type NEUndefinedOrNull = T extends null | undefined | void ? never : T; + + /** + * Include truthy (or exclude falsy) types from T + */ + export type Truthy = T extends false | null | undefined | void | 0 | "" ? never : T; + + /** + * Include falsy from T + */ + export type Falsy = T extends false | null | undefined | void | 0 | "" ? T : never; +} + /** * Obtain the return type of a function type */ diff --git a/tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.js b/tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.js new file mode 100644 index 0000000000000..8d94161fe3f62 --- /dev/null +++ b/tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.js @@ -0,0 +1,17 @@ +//// [strictGenericNarrowingUsesNonFalsy.ts] +function f(o: Readonly) { + if (o.x) { + o.x.toExponential(); // Hover over 'x' shows number + const n: number = o.x; // Error. Hover over 'x' shows `T["x"]` + } +} + + +//// [strictGenericNarrowingUsesNonFalsy.js] +"use strict"; +function f(o) { + if (o.x) { + o.x.toExponential(); // Hover over 'x' shows number + var n = o.x; // Error. Hover over 'x' shows `T["x"]` + } +} diff --git a/tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.symbols b/tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.symbols new file mode 100644 index 0000000000000..82b1b7c413817 --- /dev/null +++ b/tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.symbols @@ -0,0 +1,29 @@ +=== tests/cases/compiler/strictGenericNarrowingUsesNonFalsy.ts === +function f(o: Readonly) { +>f : Symbol(f, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 0)) +>T : Symbol(T, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 11)) +>x : Symbol(x, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 22)) +>o : Symbol(o, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 37)) +>Readonly : Symbol(Readonly, Decl(lib.d.ts, --, --)) +>T : Symbol(T, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 11)) + + if (o.x) { +>o.x : Symbol(x, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 22)) +>o : Symbol(o, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 37)) +>x : Symbol(x, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 22)) + + o.x.toExponential(); // Hover over 'x' shows number +>o.x.toExponential : Symbol(Number.toExponential, Decl(lib.d.ts, --, --)) +>o.x : Symbol(x, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 22)) +>o : Symbol(o, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 37)) +>x : Symbol(x, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 22)) +>toExponential : Symbol(Number.toExponential, Decl(lib.d.ts, --, --)) + + const n: number = o.x; // Error. Hover over 'x' shows `T["x"]` +>n : Symbol(n, Decl(strictGenericNarrowingUsesNonFalsy.ts, 3, 13)) +>o.x : Symbol(x, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 22)) +>o : Symbol(o, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 37)) +>x : Symbol(x, Decl(strictGenericNarrowingUsesNonFalsy.ts, 0, 22)) + } +} + diff --git a/tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.types b/tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.types new file mode 100644 index 0000000000000..6ac22d880942a --- /dev/null +++ b/tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.types @@ -0,0 +1,30 @@ +=== tests/cases/compiler/strictGenericNarrowingUsesNonFalsy.ts === +function f(o: Readonly) { +>f : (o: Readonly) => void +>T : T +>x : number | undefined +>o : Readonly +>Readonly : Readonly +>T : T + + if (o.x) { +>o.x : T["x"] +>o : Readonly +>x : T["x"] + + o.x.toExponential(); // Hover over 'x' shows number +>o.x.toExponential() : string +>o.x.toExponential : (fractionDigits?: number | undefined) => string +>o.x : number +>o : Readonly +>x : number +>toExponential : (fractionDigits?: number | undefined) => string + + const n: number = o.x; // Error. Hover over 'x' shows `T["x"]` +>n : number +>o.x : TypeFacts.Truthy +>o : Readonly +>x : TypeFacts.Truthy + } +} + diff --git a/tests/cases/compiler/strictGenericNarrowingUsesNonFalsy.ts b/tests/cases/compiler/strictGenericNarrowingUsesNonFalsy.ts new file mode 100644 index 0000000000000..3720a338eae16 --- /dev/null +++ b/tests/cases/compiler/strictGenericNarrowingUsesNonFalsy.ts @@ -0,0 +1,7 @@ +// @strict: true +function f(o: Readonly) { + if (o.x) { + o.x.toExponential(); // Hover over 'x' shows number + const n: number = o.x; // Error. Hover over 'x' shows `T["x"]` + } +} From cb855afc54455703fa8f78870ea83668ce5959db Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Thu, 8 Mar 2018 17:02:10 -0800 Subject: [PATCH 2/3] Fix bug in checker that created zero length intersections, try constraint based types --- src/compiler/checker.ts | 18 +++++++++++ src/lib/es5.d.ts | 70 +++++++++++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 472d02dfcc849..30429d08c80c9 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -7951,6 +7951,15 @@ namespace ts { typeSet.push(type); } } + // cases for the non-strict null and undefined - if, for example, the interection is _just_ null and/or undefined + else if (flags & TypeFlags.Nullable) { + if (flags & TypeFlags.Null) { + includes |= TypeIncludes.Null + } + if (flags & TypeFlags.Undefined) { + includes |= TypeIncludes.Undefined; + } + } return includes; } @@ -7988,6 +7997,14 @@ namespace ts { if (includes & TypeIncludes.EmptyObject && !(includes & TypeIncludes.ObjectType)) { typeSet.push(emptyObjectType); } + if (typeSet.length === 0) { + if (includes & TypeIncludes.Null) { + typeSet.push(nullType); + } + if (includes & TypeIncludes.Undefined) { + typeSet.push(undefinedType); + } + } if (typeSet.length === 1) { return typeSet[0]; } @@ -7999,6 +8016,7 @@ namespace ts { return getUnionType(map(unionType.types, t => getIntersectionType(replaceElement(typeSet, unionIndex, t))), UnionReduction.Literal, aliasSymbol, aliasTypeArguments); } + Debug.assert(typeSet.length > 1, "Intersection type must have more than one member"); const id = getTypeListId(typeSet); let type = intersectionTypes.get(id); if (!type) { diff --git a/src/lib/es5.d.ts b/src/lib/es5.d.ts index 956cc4b35598f..d243c3cd5b484 100644 --- a/src/lib/es5.d.ts +++ b/src/lib/es5.d.ts @@ -1361,35 +1361,85 @@ type Extract = T extends U ? T : never; type NonNullable = T extends null | undefined ? never : T; declare namespace TypeFacts { + /** + * Effectively replaces a `T` with a `T extends string` + */ + export type IsString = T; + + /** + * Effectively replaces a `T` with a `T extends number` + */ + export type IsNumber = T; + + /** + * Effectively replaces a `T` with a `T extends boolean` + */ + export type IsBoolean = T; + + /** + * Effectively replaces a `T` with a `T extends symbol` + */ + export type IsSymbol = T; + + /** + * Effectively replaces a `T` with a `T extends object` + */ + export type IsObject = T; + + /** + * Effectively replaces a `T` with a `T extends (...args: any[]) => any` + */ + export type IsFunction any> = T; + + /** + * Effectively replaces a `T` with a `T extends undefined | void` + */ + export type IsUndefined = T; + + /** + * Effectively replaces a `T` with a `T extends null` + */ + export type IsNull = T; + + /** + * Effectively replaces a `T` with a `T extends undefined | null | void` + */ + export type IsUndefinedOrNull = T; + + /** + * Effectively replaces a `T` with a `T extends false | null | undefined | void | 0 | ""` + */ + export type IsFalsy = T; + /** * Include only string from T */ - export type EQString = T extends string ? T : never; + export type EQString = T extends IsString ? T&U : never; /** * Include only number from T */ - export type EQNumber = T extends number ? T : never; + export type EQNumber = T extends IsNumber ? T&U : never; /** * Include only boolean from T */ - export type EQBoolean = T extends boolean ? T : never; + export type EQBoolean = T extends IsBoolean ? T&U : never; /** * Include only symbol from T */ - export type EQSymbol = T extends symbol ? T : never; + export type EQSymbol = T extends IsSymbol ? T&U : never; /** * Include only object from T */ - export type EQObject = T extends object ? T : never; + export type EQObject = T extends IsObject ? T&U : never; /** * Include only functions from T */ - export type EQFunction = T extends (...args: any[]) => any ? T : never; + export type EQFunction = T extends IsFunction ? T&U : never; /** * Exclude only string from T @@ -1424,17 +1474,17 @@ declare namespace TypeFacts { /** * Include only undefined from T */ - export type EQUndefined = T extends undefined | void ? T : never; + export type EQUndefined = T extends IsUndefined ? T&U : never; /** * Include only null from T */ - export type EQNull = T extends null ? T : never; + export type EQNull = T extends IsNull ? T&U : never; /** * Include only null and undefined from T */ - export type EQUndefinedOrNull = T extends null | undefined | void ? T : never; + export type EQUndefinedOrNull = T extends IsUndefinedOrNull ? T&U : never; /** * Exclude only undefined from T @@ -1459,7 +1509,7 @@ declare namespace TypeFacts { /** * Include falsy from T */ - export type Falsy = T extends false | null | undefined | void | 0 | "" ? T : never; + export type Falsy = T extends IsFalsy ? T&U : never; } /** From abc5684aac960c42def2cd16ef78368b14cb02c3 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 8 May 2018 18:32:37 -0700 Subject: [PATCH 3/3] A break and a (maybe) bug --- src/compiler/factory.ts | 2 +- src/harness/collections.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index e8916ac62e51e..cd952ada489d0 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -2705,7 +2705,7 @@ namespace ts { } function asToken(value: TKind | Token): Token { - return typeof value === "number" ? createToken(value) : value; + return typeof value === "number" ? createToken(value) : value as any; // TODO: FIXME } /** diff --git a/src/harness/collections.ts b/src/harness/collections.ts index 7c71296409c98..32afc89505454 100644 --- a/src/harness/collections.ts +++ b/src/harness/collections.ts @@ -13,8 +13,8 @@ namespace collections { private _copyOnWrite = false; constructor(comparer: ((a: K, b: K) => number) | SortOptions, iterable?: Iterable<[K, V]>) { - this._comparer = typeof comparer === "object" ? comparer.comparer : comparer; - this._order = typeof comparer === "object" && comparer.sort === "insertion" ? [] : undefined; + this._comparer = typeof comparer === "function" ? comparer : comparer.comparer; + this._order = typeof comparer !== "function" && comparer.sort === "insertion" ? [] : undefined; if (iterable) { const iterator = getIterator(iterable); try {