Skip to content

Commit

Permalink
Further restrict 'Awaited<T>' auto-wrapping for 'await'
Browse files Browse the repository at this point in the history
  • Loading branch information
rbuckton committed Sep 9, 2021
1 parent 657f0a9 commit 6703b9f
Show file tree
Hide file tree
Showing 9 changed files with 932 additions and 41 deletions.
60 changes: 36 additions & 24 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13469,7 +13469,6 @@ namespace ts {
}

function getGlobalAwaitedSymbol(reportErrors: boolean): Symbol | undefined {
if (reportErrors) debugger;
// Only cache `unknownSymbol` if we are reporting errors so that we don't report the error more than once.
deferredGlobalAwaitedSymbol ||= getGlobalTypeAliasSymbol("Awaited" as __String, /*arity*/ 1, reportErrors) || (reportErrors ? unknownSymbol : undefined);
return deferredGlobalAwaitedSymbol === unknownSymbol ? undefined : deferredGlobalAwaitedSymbol;
Expand Down Expand Up @@ -32560,10 +32559,8 @@ namespace ts {
let wouldWorkWithAwait = false;
const errNode = errorNode || operatorToken;
if (isRelated) {
let awaitedLeftType = getAwaitedType(leftType);
let awaitedRightType = getAwaitedType(rightType);
awaitedLeftType &&= unwrapAwaitedType(awaitedLeftType);
awaitedRightType &&= unwrapAwaitedType(awaitedRightType);
const awaitedLeftType = unwrapAwaitedType(getAwaitedType(leftType));
const awaitedRightType = unwrapAwaitedType(getAwaitedType(rightType));
wouldWorkWithAwait = !(awaitedLeftType === leftType && awaitedRightType === rightType)
&& !!(awaitedLeftType && awaitedRightType)
&& isRelated(awaitedLeftType, awaitedRightType);
Expand Down Expand Up @@ -34643,9 +34640,14 @@ namespace ts {
}

/**
* Determines whether a type has a callable `then` member.
* Determines whether a type is an object with a callable `then` member.
*/
function isThenableType(type: Type): boolean {
if (allTypesAssignableToKind(type, TypeFlags.Primitive | TypeFlags.Never)) {
// primitive types cannot be considered "thenable" since they are not objects.
return false;
}

const thenFunction = getTypeOfPropertyOfType(type, "then" as __String);
return !!thenFunction && getSignaturesOfType(getTypeWithFacts(thenFunction, TypeFacts.NEUndefinedOrNull), SignatureKind.Call).length > 0;
}
Expand All @@ -34667,13 +34669,24 @@ namespace ts {
/**
* For a generic `Awaited<T>`, gets `T`.
*/
function unwrapAwaitedType(type: Type) {
function unwrapAwaitedType(type: Type): Type;
function unwrapAwaitedType(type: Type | undefined): Type | undefined;
function unwrapAwaitedType(type: Type | undefined) {
if (!type) return undefined;
return type.flags & TypeFlags.Union ? mapType(type, unwrapAwaitedType) :
isAwaitedTypeInstantiation(type) ? type.aliasTypeArguments[0] :
type;
}

function createAwaitedTypeIfNeeded(type: Type): Type {
// We wrap type `T` in `Awaited<T>` based on the following conditions:
// - `T` is not already an `Awaited<U>`, and
// - `T` is generic, and
// - One of the following applies:
// - `T` has no base constraint, or
// - The base constraint of `T` is `any`, `unknown`, `object`, or `{}`, or
// - The base constraint of `T` is an object type with a callable `then` method.

if (isTypeAny(type)) {
return type;
}
Expand All @@ -34684,13 +34697,18 @@ namespace ts {
}

// Only instantiate `Awaited<T>` if `T` contains possibly non-primitive types.
if (isGenericObjectType(type) && !allTypesAssignableToKind(type, TypeFlags.Primitive | TypeFlags.Never)) {
// Nothing to do if `Awaited<T>` doesn't exist
const awaitedSymbol = getGlobalAwaitedSymbol(/*reportErrors*/ true);
if (awaitedSymbol) {
// Unwrap unions that may contain `Awaited<T>`, otherwise its possible to manufacture an `Awaited<Awaited<T> | U>` where
// an `Awaited<T | U>` would suffice.
return getTypeAliasInstantiation(awaitedSymbol, [unwrapAwaitedType(type)]);
if (isGenericObjectType(type)) {
const baseConstraint = getBaseConstraintOfType(type);
// Only instantiate `Awaited<T>` if `T` has no base constraint, or the base constraint of `T` is `any`, `unknown`, `{}`, `object`,
// or is promise-like.
if (!baseConstraint || (baseConstraint.flags & TypeFlags.AnyOrUnknown) || isEmptyObjectType(baseConstraint) || isThenableType(baseConstraint)) {
// Nothing to do if `Awaited<T>` doesn't exist
const awaitedSymbol = getGlobalAwaitedSymbol(/*reportErrors*/ true);
if (awaitedSymbol) {
// Unwrap unions that may contain `Awaited<T>`, otherwise its possible to manufacture an `Awaited<Awaited<T> | U>` where
// an `Awaited<T | U>` would suffice.
return getTypeAliasInstantiation(awaitedSymbol, [unwrapAwaitedType(type)]);
}
}
}

Expand Down Expand Up @@ -34731,12 +34749,6 @@ namespace ts {
return typeAsAwaitable.awaitedTypeOfType && createAwaitedTypeIfNeeded(typeAsAwaitable.awaitedTypeOfType);
}

// primitives with a `{ then() }` won't be unwrapped/adopted. This prevents `Awaited<T>` when `T extends string`
// (or another primitive), since the `Awaited<T>` type only unwraps `object` types.
if (allTypesAssignableToKind(type, TypeFlags.Primitive | TypeFlags.Never)) {
return type;
}

const promisedType = getPromisedTypeOfPromise(type);
if (promisedType) {
if (type.id === promisedType.id || awaitedTypeStack.lastIndexOf(promisedType.id) >= 0) {
Expand Down Expand Up @@ -34865,7 +34877,7 @@ namespace ts {
if (globalPromiseType !== emptyGenericType && !isReferenceToType(returnType, globalPromiseType)) {
// The promise type was not a valid type reference to the global promise type, so we
// report an error and return the unknown type.
error(returnTypeNode, Diagnostics.The_return_type_of_an_async_function_or_method_must_be_the_global_Promise_T_type_Did_you_mean_to_write_Promise_0, typeToString(unwrapAwaitedType(getAwaitedType(returnType) || voidType)));
error(returnTypeNode, Diagnostics.The_return_type_of_an_async_function_or_method_must_be_the_global_Promise_T_type_Did_you_mean_to_write_Promise_0, typeToString(unwrapAwaitedType(getAwaitedType(returnType)) || voidType));
return;
}
}
Expand Down Expand Up @@ -36865,7 +36877,7 @@ namespace ts {
// - `Generator<T, TReturn, TNext>` or `AsyncGenerator<T, TReturn, TNext>`
if (isReferenceToType(type, resolver.getGlobalGeneratorType(/*reportErrors*/ false))) {
const [yieldType, returnType, nextType] = getTypeArguments(type as GenericType);
return setCachedIterationTypes(type, resolver.iterableCacheKey, createIterationTypes(yieldType, returnType, nextType));
return setCachedIterationTypes(type, resolver.iterableCacheKey, createIterationTypes(resolver.resolveIterationType(yieldType, /*errorNode*/ undefined) || yieldType, resolver.resolveIterationType(returnType, /*errorNode*/ undefined) || returnType, nextType));
}
}

Expand Down Expand Up @@ -37197,8 +37209,8 @@ namespace ts {
function unwrapReturnType(returnType: Type, functionFlags: FunctionFlags) {
const isGenerator = !!(functionFlags & FunctionFlags.Generator);
const isAsync = !!(functionFlags & FunctionFlags.Async);
return isGenerator ? getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, returnType, isAsync) ?? errorType :
isAsync ? unwrapAwaitedType(getAwaitedType(returnType) ?? errorType) :
return isGenerator ? getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, returnType, isAsync) || errorType :
isAsync ? unwrapAwaitedType(getAwaitedType(returnType)) || errorType :
returnType;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ class C {
>method : () => void

var fn = async () => await this;
>fn : () => Promise<Awaited<this>>
>async () => await this : () => Promise<Awaited<this>>
>await this : Awaited<this>
>fn : () => Promise<this>
>async () => await this : () => Promise<this>
>await this : this
>this : this
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ class C {
>method : () => void

var fn = async () => await this;
>fn : () => Promise<Awaited<this>>
>async () => await this : () => Promise<Awaited<this>>
>await this : Awaited<this>
>fn : () => Promise<this>
>async () => await this : () => Promise<this>
>await this : this
>this : this
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ class C {
>method : () => void

var fn = async () => await this;
>fn : () => Promise<Awaited<this>>
>async () => await this : () => Promise<Awaited<this>>
>await this : Awaited<this>
>fn : () => Promise<this>
>async () => await this : () => Promise<this>
>await this : this
>this : this
}
}
Expand Down
116 changes: 116 additions & 0 deletions tests/baselines/reference/awaitedType.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,122 @@ tests/cases/compiler/awaitedType.ts(22,12): error TS2589: Type instantiation is
])
}

// non-generic
async function f1(x: string) {
// y: string
const y = await x;
}

async function f2(x: unknown) {
// y: unknown
const y = await x;
}

async function f3(x: object) {
// y: object
const y = await x;
}

async function f4(x: Promise<string>) {
// y: string
const y = await x;
}

async function f5(x: Promise<unknown>) {
// y: unknown
const y = await x;
}

async function f6(x: Promise<object>) {
// y: object
const y = await x;
}

// generic

async function f7<T>(x: T) {
// NOTE: T does not belong solely to the domain of primitive types and either does
// not have a base constraint, its base constraint is `any`, `unknown`, `{}`, or `object`,
// or it has a non-primitive base constraint with a callable `then`.

// y: Awaited<T>
const y = await x;
}

async function f8<T extends any>(x: T) {
// NOTE: T does not belong solely to the domain of primitive types and either does
// not have a base constraint, its base constraint is `any`, `unknown`, `{}`, or `object`,
// or it has a non-primitive base constraint with a callable `then`.

// y: Awaited<T>
const y = await x;
}

async function f9<T extends unknown>(x: T) {
// NOTE: T does not belong solely to the domain of primitive types and either does
// not have a base constraint, its base constraint is `any`, `unknown`, `{}`, or `object`,
// or it has a non-primitive base constraint with a callable `then`.

// y: Awaited<T>
const y = await x;
}

async function f10<T extends {}>(x: T) {
// NOTE: T does not belong solely to the domain of primitive types and either does
// not have a base constraint, its base constraint is `any`, `unknown`, `{}`, or `object`,
// or it has a non-primitive base constraint with a callable `then`.

// y: Awaited<T>
const y = await x;
}

async function f11<T extends { then(onfulfilled: (value: unknown) => void): void }>(x: T) {
// NOTE: T does not belong solely to the domain of primitive types and either does
// not have a base constraint, its base constraint is `any`, `unknown`, `{}`, or `object`,
// or it has a non-primitive base constraint with a callable `then`.

// y: Awaited<T>
const y = await x;
}

async function f12<T extends string | object>(x: T) {
// NOTE: T does not belong solely to the domain of primitive types and either does
// not have a base constraint, its base constraint is `any`, `unknown`, `{}`, or `object`,
// or it has a non-primitive base constraint with a callable `then`.

// y: Awaited<T>
const y = await x;
}

async function f13<T extends string>(x: T) {
// NOTE: T belongs to the domain of primitive types

// y: T
const y = await x;
}

async function f14<T extends { x: number }>(x: T) {
// NOTE: T has a non-primitive base constraint without a callable `then`.

// y: T
const y = await x;
}

async function f15<T extends { then: number }>(x: T) {
// NOTE: T has a non-primitive base constraint without a callable `then`.

// y: T
const y = await x;
}

async function f16<T extends number & { then(): void }>(x: T) {
// NOTE: T belongs to the domain of primitive types (regardless of `then`)

// y: T
const y = await x;
}


// helps with tests where '.types' just prints out the type alias name
type _Expect<TActual extends TExpected, TExpected> = TActual;

Loading

0 comments on commit 6703b9f

Please sign in to comment.