Skip to content
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

Use lib conditional types for type facts if possible #22348

Closed
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
61 changes: 61 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,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
Expand Down Expand Up @@ -484,6 +485,29 @@ namespace ts {
// Suggestion diagnostics must have a file. Keyed by source file name.
const suggestionDiagnostics = createMultiMap<Diagnostic>();

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"
Expand Down Expand Up @@ -8396,6 +8420,14 @@ namespace ts {
if (includes & TypeFlags.EmptyObject && !(includes & TypeFlags.Object)) {
typeSet.push(emptyObjectType);
}
if (typeSet.length === 0) {
if (includes & TypeFlags.Null) {
typeSet.push(nullType);
}
if (includes & TypeFlags.Undefined) {
typeSet.push(undefinedType);
}
}
if (typeSet.length === 1) {
return typeSet[0];
}
Expand All @@ -8407,6 +8439,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) {
Expand Down Expand Up @@ -12953,7 +12986,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);
}

Expand Down
2 changes: 1 addition & 1 deletion src/compiler/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2705,7 +2705,7 @@ namespace ts {
}

function asToken<TKind extends SyntaxKind>(value: TKind | Token<TKind>): Token<TKind> {
return typeof value === "number" ? createToken(value) : value;
return typeof value === "number" ? createToken(value) : value as any; // TODO: FIXME
Copy link
Member

Choose a reason for hiding this comment

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

What's this about?

Copy link
Member Author

Choose a reason for hiding this comment

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

The typeof narrows to TKind in the true branch, but the false branch narrows to NENumber<TKind | Token<TKind>> without simplifying even though it obviously should (since generic conditionals don't get simplified when distributive), IIRC. Would have to look at it again to know for sure, though. Might not even repro after a merge - it's been a bit and we've iterated in conditional types quite a bit.

}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/harness/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ namespace collections {
private _copyOnWrite = false;

constructor(comparer: ((a: K, b: K) => number) | SortOptions<K>, 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 {
Expand Down
2 changes: 1 addition & 1 deletion src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3299,7 +3299,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));
Copy link
Member

Choose a reason for hiding this comment

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

🤣

Copy link
Member Author

Choose a reason for hiding this comment

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

Right? Tfw the change finds a real error in the code.

}
}

Expand Down
152 changes: 152 additions & 0 deletions src/lib/es5.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,158 @@ type Extract<T, U> = T extends U ? T : never;
*/
type NonNullable<T> = T extends null | undefined ? never : T;

declare namespace TypeFacts {
/**
* Effectively replaces a `T` with a `T extends string`
*/
export type IsString<T extends string> = T;

/**
* Effectively replaces a `T` with a `T extends number`
*/
export type IsNumber<T extends number> = T;

/**
* Effectively replaces a `T` with a `T extends boolean`
*/
export type IsBoolean<T extends boolean> = T;

/**
* Effectively replaces a `T` with a `T extends symbol`
*/
export type IsSymbol<T extends symbol> = T;

/**
* Effectively replaces a `T` with a `T extends object`
*/
export type IsObject<T extends object> = T;

/**
* Effectively replaces a `T` with a `T extends (...args: any[]) => any`
*/
export type IsFunction<T extends (...args: any[]) => any> = T;

/**
* Effectively replaces a `T` with a `T extends undefined | void`
*/
export type IsUndefined<T extends undefined | void> = T;

/**
* Effectively replaces a `T` with a `T extends null`
*/
export type IsNull<T extends null> = T;

/**
* Effectively replaces a `T` with a `T extends undefined | null | void`
*/
export type IsUndefinedOrNull<T extends undefined | null | void> = T;

/**
* Effectively replaces a `T` with a `T extends false | null | undefined | void | 0 | ""`
*/
export type IsFalsy<T extends false | null | undefined | void | 0 | ""> = T;

/**
* Include only string from T
*/
export type EQString<T> = T extends IsString<infer U> ? T&U : never;

/**
* Include only number from T
*/
export type EQNumber<T> = T extends IsNumber<infer U> ? T&U : never;

/**
* Include only boolean from T
*/
export type EQBoolean<T> = T extends IsBoolean<infer U> ? T&U : never;

/**
* Include only symbol from T
*/
export type EQSymbol<T> = T extends IsSymbol<infer U> ? T&U : never;

/**
* Include only object from T
*/
export type EQObject<T> = T extends IsObject<infer U> ? T&U : never;

/**
* Include only functions from T
*/
export type EQFunction<T> = T extends IsFunction<infer U> ? T&U : never;

/**
* Exclude only string from T
*/
export type NEString<T> = T extends string ? never : T;

/**
* Exclude only number from T
*/
export type NENumber<T> = T extends number ? never : T;

/**
* Exclude only boolean from T
*/
export type NEBoolean<T> = T extends boolean ? never : T;

/**
* Exclude only symbol from T
*/
export type NESymbol<T> = T extends symbol ? never : T;

/**
* Exclude only object from T
*/
export type NEObject<T> = T extends object ? never : T;

/**
* Exclude only functions from T
*/
export type NEFunction<T> = T extends (...args: any[]) => any ? never : T;

/**
* Include only undefined from T
*/
export type EQUndefined<T> = T extends IsUndefined<infer U> ? T&U : never;

/**
* Include only null from T
*/
export type EQNull<T> = T extends IsNull<infer U> ? T&U : never;

/**
* Include only null and undefined from T
*/
export type EQUndefinedOrNull<T> = T extends IsUndefinedOrNull<infer U> ? T&U : never;

/**
* Exclude only undefined from T
*/
export type NEUndefined<T> = T extends undefined | void ? never : T;

/**
* Exclude only null from T
*/
export type NENull<T> = T extends null ? never : T;

/**
* Exclude null and undefined from T (different from NonNullable as it also filters void)
*/
export type NEUndefinedOrNull<T> = T extends null | undefined | void ? never : T;

/**
* Include truthy (or exclude falsy) types from T
*/
export type Truthy<T> = T extends false | null | undefined | void | 0 | "" ? never : T;

/**
* Include falsy from T
*/
export type Falsy<T> = T extends IsFalsy<infer U> ? T&U : never;
}

/**
* Obtain the return type of a function type
*/
Expand Down
17 changes: 17 additions & 0 deletions tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//// [strictGenericNarrowingUsesNonFalsy.ts]
function f<T extends { x?: number }>(o: Readonly<T>) {
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"]`
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
=== tests/cases/compiler/strictGenericNarrowingUsesNonFalsy.ts ===
function f<T extends { x?: number }>(o: Readonly<T>) {
>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))
}
}

30 changes: 30 additions & 0 deletions tests/baselines/reference/strictGenericNarrowingUsesNonFalsy.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
=== tests/cases/compiler/strictGenericNarrowingUsesNonFalsy.ts ===
function f<T extends { x?: number }>(o: Readonly<T>) {
>f : <T extends { x?: number | undefined; }>(o: Readonly<T>) => void
>T : T
>x : number | undefined
>o : Readonly<T>
>Readonly : Readonly<T>
>T : T

if (o.x) {
>o.x : T["x"]
>o : Readonly<T>
>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<T>
>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<T["x"]>
>o : Readonly<T>
>x : TypeFacts.Truthy<T["x"]>
}
}

7 changes: 7 additions & 0 deletions tests/cases/compiler/strictGenericNarrowingUsesNonFalsy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// @strict: true
function f<T extends { x?: number }>(o: Readonly<T>) {
if (o.x) {
o.x.toExponential(); // Hover over 'x' shows number
const n: number = o.x; // Error. Hover over 'x' shows `T["x"]`
}
}