diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 09f38832fe3b3..694ef723fd971 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -1787,6 +1787,11 @@ export class Checker { return type.getApparentType(); } + /** Get the reduced type of a type. Always returns a type. */ + async getReducedType(type: Type): Promise { + return type.getReducedType(); + } + async getPropertiesOfType(type: Type): Promise { return type.getProperties(); } @@ -2237,6 +2242,7 @@ class TypeObject implements Type { private default: number | false; private nonNullableType: number | false; private apparentType: number | false; + private reducedType: number | false; private properties: readonly Symbol[] | false; private apparentProperties: readonly Symbol[] | false; private callSignatures: readonly Signature[] | false; @@ -2286,6 +2292,7 @@ class TypeObject implements Type { this.default = false; this.nonNullableType = false; this.apparentType = false; + this.reducedType = false; this.properties = false; this.apparentProperties = false; this.callSignatures = false; @@ -2368,6 +2375,12 @@ class TypeObject implements Type { return result; } + async getReducedType(): Promise { + const result = await this.objectRegistry.fetchType(this, "getReducedType", this.reducedType); + this.reducedType = result.id; + return result; + } + async getIndexInfos(): Promise { if (this.indexInfos === false) { this.indexInfos = await this.objectRegistry.fetchIndexInfosOfType(this); diff --git a/packages/typescript/src/api/async/types.ts b/packages/typescript/src/api/async/types.ts index adc64768c2452..62b3dbdeb6e36 100644 --- a/packages/typescript/src/api/async/types.ts +++ b/packages/typescript/src/api/async/types.ts @@ -47,6 +47,9 @@ export interface Type { /** Get the apparent type of this type. */ getApparentType(): Promise; + /** Get the reduced type of this type. */ + getReducedType(): Promise; + /** Get the call signatures of this type. */ getCallSignatures(): Promise; diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 5e30d12bae247..35c08679d2aa4 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -91,6 +91,7 @@ export interface APIMethodInfo { getPropertiesOfType: APIMethod; getApparentPropertiesOfType: APIMethod; getApparentType: APIMethod; + getReducedType: APIMethod; getPropertyOfType: APIMethod; getIndexInfosOfType: APIMethod; getConstraintOfTypeParameter: APIMethod; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 582c0b6c54359..6379e068d9f20 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -1795,6 +1795,11 @@ export class Checker { return type.getApparentType(); } + /** Get the reduced type of a type. Always returns a type. */ + getReducedType(type: Type): Type { + return type.getReducedType(); + } + getPropertiesOfType(type: Type): readonly Symbol[] { return type.getProperties(); } @@ -2245,6 +2250,7 @@ class TypeObject implements Type { private default: number | false; private nonNullableType: number | false; private apparentType: number | false; + private reducedType: number | false; private properties: readonly Symbol[] | false; private apparentProperties: readonly Symbol[] | false; private callSignatures: readonly Signature[] | false; @@ -2294,6 +2300,7 @@ class TypeObject implements Type { this.default = false; this.nonNullableType = false; this.apparentType = false; + this.reducedType = false; this.properties = false; this.apparentProperties = false; this.callSignatures = false; @@ -2376,6 +2383,12 @@ class TypeObject implements Type { return result; } + getReducedType(): Type { + const result = this.objectRegistry.fetchType(this, "getReducedType", this.reducedType); + this.reducedType = result.id; + return result; + } + getIndexInfos(): readonly IndexInfo[] { if (this.indexInfos === false) { this.indexInfos = this.objectRegistry.fetchIndexInfosOfType(this); diff --git a/packages/typescript/src/api/sync/types.ts b/packages/typescript/src/api/sync/types.ts index 04e9f3f848577..a65b159412c14 100644 --- a/packages/typescript/src/api/sync/types.ts +++ b/packages/typescript/src/api/sync/types.ts @@ -55,6 +55,9 @@ export interface Type { /** Get the apparent type of this type. */ getApparentType(): Type; + /** Get the reduced type of this type. */ + getReducedType(): Type; + /** Get the call signatures of this type. */ getCallSignatures(): readonly Signature[]; diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 24ae9c84f1eb8..7acef882f8cae 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -818,6 +818,47 @@ describe("Checker - getApparentType", () => { }); }); +describe("Checker - getReducedType", () => { + test("returns the reduced type", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +declare const TaggedError: ( + tag: Tag, +) => new = {}>( + args: { readonly [P in keyof A]: A[P] }, +) => { readonly _tag: Tag } & Readonly; + +class RateLimitError extends TaggedError("RateLimitError")<{ + readonly retryAfter: number; +}> {} + +class QuotaExceededError extends TaggedError("QuotaExceededError")<{ + readonly limit: number; +}> {} + +export type Result = RateLimitError | (RateLimitError & QuotaExceededError);`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = await project.program.getSourceFile("/src/main.ts"); + assert.ok(sourceFile); + const typeAlias = sourceFile.statements.find(isTypeAliasDeclaration); + assert.ok(typeAlias); + const type = await project.checker.getTypeAtLocation(typeAlias); + assert.equal(type.isUnionType(), true); + assert.equal(type.isObjectType(), false); + const reducedType = await project.checker.getReducedType(type); + assert.equal(reducedType.isUnionType(), false); + assert.equal(reducedType.isObjectType(), true); + } + finally { + await api.close(); + } + }); +}); + describe("Checker - getMemberInModuleExports", () => { test("returns a named export when present", async () => { const api = spawnAPI({ diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 93fe5eae5255f..f409e18270033 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -826,6 +826,47 @@ describe("Checker - getApparentType", () => { }); }); +describe("Checker - getReducedType", () => { + test("returns the reduced type", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), + "/src/main.ts": ` +declare const TaggedError: ( + tag: Tag, +) => new = {}>( + args: { readonly [P in keyof A]: A[P] }, +) => { readonly _tag: Tag } & Readonly; + +class RateLimitError extends TaggedError("RateLimitError")<{ + readonly retryAfter: number; +}> {} + +class QuotaExceededError extends TaggedError("QuotaExceededError")<{ + readonly limit: number; +}> {} + +export type Result = RateLimitError | (RateLimitError & QuotaExceededError);`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const sourceFile = project.program.getSourceFile("/src/main.ts"); + assert.ok(sourceFile); + const typeAlias = sourceFile.statements.find(isTypeAliasDeclaration); + assert.ok(typeAlias); + const type = project.checker.getTypeAtLocation(typeAlias); + assert.equal(type.isUnionType(), true); + assert.equal(type.isObjectType(), false); + const reducedType = project.checker.getReducedType(type); + assert.equal(reducedType.isUnionType(), false); + assert.equal(reducedType.isObjectType(), true); + } + finally { + api.close(); + } + }); +}); + describe("Checker - getMemberInModuleExports", () => { test("returns a named export when present", () => { const api = spawnAPI({ diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 8178bf0cbef04..45793d96a61be 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -149,6 +149,7 @@ const ( MethodGetPropertiesOfType Method = "getPropertiesOfType" MethodGetApparentPropertiesOfType Method = "getApparentPropertiesOfType" MethodGetApparentType Method = "getApparentType" + MethodGetReducedType Method = "getReducedType" MethodGetPropertyOfType Method = "getPropertyOfType" MethodGetIndexInfosOfType Method = "getIndexInfosOfType" MethodGetConstraintOfTypeParameter Method = "getConstraintOfTypeParameter" @@ -487,6 +488,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetPropertiesOfType: unmarshallerFor[CheckerTypeParams], MethodGetApparentPropertiesOfType: unmarshallerFor[GetTypePropertyParams], MethodGetApparentType: unmarshallerFor[GetTypePropertyParams], + MethodGetReducedType: unmarshallerFor[GetTypePropertyParams], MethodGetPropertyOfType: unmarshallerFor[GetPropertyOfTypeParams], MethodGetIndexInfosOfType: unmarshallerFor[CheckerTypeParams], MethodGetConstraintOfTypeParameter: unmarshallerFor[GetTypePropertyParams], diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index e87e02c9b7ca3..d5c668385b849 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -777,6 +777,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetApparentPropertiesOfType(ctx, parsed.(*GetTypePropertyParams)) case string(MethodGetApparentType): return s.handleGetApparentType(ctx, parsed.(*GetTypePropertyParams)) + case string(MethodGetReducedType): + return s.handleGetReducedType(ctx, parsed.(*GetTypePropertyParams)) case string(MethodGetPropertyOfType): return s.handleGetPropertyOfType(ctx, parsed.(*GetPropertyOfTypeParams)) case string(MethodGetIndexInfosOfType): @@ -3062,6 +3064,22 @@ func (s *Session) handleGetApparentType(ctx context.Context, params *GetTypeProp return setup.newTypeResponse(setup.checker.GetApparentType(t)), nil } +// handleGetReducedType returns the reduced type of a type. +func (s *Session) handleGetReducedType(ctx context.Context, params *GetTypePropertyParams) (*TypeResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + t, err := setup.resolveTypeHandle(params.Type) + if err != nil { + return nil, err + } + + return setup.newTypeResponse(setup.checker.GetReducedType(t)), nil +} + // handleGetIndexInfosOfType returns the index infos of a type. // @gen-proto-nullable func (s *Session) handleGetIndexInfosOfType(ctx context.Context, params *CheckerTypeParams) ([]*IndexInfoResponse, error) { diff --git a/tsc/internal/checker/exports.go b/tsc/internal/checker/exports.go index c7408156d1c3f..49efc3208d430 100644 --- a/tsc/internal/checker/exports.go +++ b/tsc/internal/checker/exports.go @@ -294,6 +294,10 @@ func (c *Checker) GetApparentType(t *Type) *Type { return c.getApparentType(t) } +func (c *Checker) GetReducedType(t *Type) *Type { + return c.getReducedType(t) +} + // GetFullyQualifiedName returns the fully qualified name of a symbol, walking up // its parent chain (e.g. `"/path/to/module".Namespace.Name`). func (c *Checker) GetFullyQualifiedName(symbol *ast.Symbol) string {