Skip to content

Commit

Permalink
feat(unions): improve type inference and remove deprecated syntax
Browse files Browse the repository at this point in the history
- remove deprecated direct array syntax
- fix TS union type when type classes extending themselves
  • Loading branch information
MichalLytek committed Mar 29, 2020
1 parent a211550 commit 2e5f155
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 25 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- **Breaking Change**: update `graphql-query-complexity` dependency to `^0.4.1` and drop support for `fieldConfigEstimator` (use `fieldExtensionsEstimator` instead)
- **Breaking Change**: introduce `sortedSchema` option in `PrintSchemaOptions` and emit sorted schema file by default
- **Breaking Change**: make `class-validator` an optional, peer dependency (#366)
- **Breaking Change**: remove deprecated direct array syntax for declaring union types
- update `TypeResolver` interface to match with `GraphQLTypeResolver` from `graphql-js`
- add basic support for directives with `@Directive()` decorator (#369)
- add possibility to tune up the performance and disable auth & middlewares stack for simple field resolvers (#479)
Expand All @@ -24,6 +25,7 @@
- fix using shared union type in multiple schemas
- fix using shared interface type in multiple schemas
- fix calling field resolver without providing resolver class to `buildSchema`
- fix generated TS union type for union type of object type classes extending themselves (#587)
### Others
- **Breaking Change**: change build config to ES2018 - drop support for Node.js < 10.3
- **Breaking Change**: remove deprecated `DepreciationOptions` interface
Expand Down
10 changes: 5 additions & 5 deletions docs/unions.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,19 @@ class Actor {
}
```

Now let's create a union type from the object types above:
Now let's create an union type from the object types above - the rarely seen `[ ] as const` syntax is to inform TypeScript compiler that it's a tuple, which allows for better TS union type inference:

```typescript
import { createUnionType } from "type-graphql";

const SearchResultUnion = createUnionType({
name: "SearchResult", // the name of the GraphQL union
types: () => [Movie, Actor], // function that returns array of object types classes
types: () => [Movie, Actor] as const, // function that returns tuple of object types classes
});
```

Now we can use the union type in the query.
Notice, that due to TypeScript's reflection limitation, we have to explicitly use the `SearchResultUnion` value in the `@Query` decorator return type annotation.
Then we can use the union type in the query by providing the `SearchResultUnion` value in the `@Query` decorator return type annotation.
Notice, that we have to explicitly use the decorator return type annotation due to TypeScript's reflection limitations.
For TypeScript compile-time type safety we can also use `typeof SearchResultUnion` which is equal to type `Movie | Actor`.

```typescript
Expand All @@ -69,7 +69,7 @@ However, we can also provide our own `resolveType` function implementation to th
```typescript
const SearchResultUnion = createUnionType({
name: "SearchResult",
types: () => [Movie, Actor],
types: () => [Movie, Actor] as const,
// our implementation of detecting returned object type
resolveType: value => {
if ("rating" in value) {
Expand Down
2 changes: 1 addition & 1 deletion examples/enums-and-unions/search-result.union.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import { Cook } from "./cook.type";

export const SearchResult = createUnionType({
name: "SearchResult",
types: () => [Recipe, Cook],
types: () => [Recipe, Cook] as const,
});
17 changes: 5 additions & 12 deletions src/decorators/unions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,26 @@ import { getMetadataStorage } from "../metadata/getMetadataStorage";
import { UnionFromClasses } from "../helpers/utils";
import { ResolveTypeOptions } from "./types";

export interface UnionTypeConfig<TClassTypes extends ClassType[]>
export interface UnionTypeConfig<TClassTypes extends readonly ClassType[]>
extends ResolveTypeOptions<UnionFromClasses<TClassTypes>> {
name: string;
description?: string;
/**
* The direct array syntax is deprecated.
* Use the function syntax `() => TClassTypes` instead.
*/
types: TClassTypes | (() => TClassTypes);
types: () => TClassTypes;
}

export function createUnionType<T extends ClassType[]>(
export function createUnionType<T extends readonly ClassType[]>(
config: UnionTypeConfig<T>,
): UnionFromClasses<T>;
export function createUnionType({
name,
description,
types: classTypesOrClassTypesFn,
types,
resolveType,
}: UnionTypeConfig<ClassType[]>): any {
const unionMetadataSymbol = getMetadataStorage().collectUnionMetadata({
name,
description,
getClassTypes:
typeof classTypesOrClassTypesFn === "function"
? classTypesOrClassTypesFn
: () => classTypesOrClassTypesFn,
getClassTypes: types,
resolveType,
});
return unionMetadataSymbol;
Expand Down
6 changes: 4 additions & 2 deletions src/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export type ArrayElements<TArray extends any[]> = TArray extends Array<infer TElement>
export type ArrayElements<TArray extends ReadonlyArray<any>> = TArray extends ReadonlyArray<
infer TElement
>
? TElement
: never;

export type UnionFromClasses<TClassesArray extends any[]> = InstanceType<
export type UnionFromClasses<TClassesArray extends ReadonlyArray<any>> = InstanceType<
ArrayElements<TClassesArray>
>;
4 changes: 2 additions & 2 deletions tests/functional/typedefs-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,13 @@ describe("buildTypeDefsAndResolvers", () => {
registerEnumType(SampleStringEnum, { name: "SampleStringEnum" });

const SampleUnion = createUnionType({
types: [SampleType2, SampleType3],
types: () => [SampleType2, SampleType3],
name: "SampleUnion",
description: "SampleUnion description",
});

const SampleResolveUnion = createUnionType({
types: [SampleType2, SampleType3],
types: () => [SampleType2, SampleType3],
name: "SampleResolveUnion",
resolveType: value => {
if ("sampleType2StringField" in value) {
Expand Down
30 changes: 27 additions & 3 deletions tests/functional/unions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe("Unions", () => {
const OneTwoThreeUnion = createUnionType({
name: "OneTwoThreeUnion",
description: "OneTwoThreeUnion description",
types: [ObjectOne, ObjectTwo, ObjectThree],
types: () => [ObjectOne, ObjectTwo, ObjectThree],
});
const OneTwoThreeUnionFn = createUnionType({
name: "OneTwoThreeUnionFn",
Expand All @@ -50,7 +50,7 @@ describe("Unions", () => {

const UnionWithStringResolveType = createUnionType({
name: "UnionWithStringResolveType",
types: [ObjectOne, ObjectTwo],
types: () => [ObjectOne, ObjectTwo],
resolveType: value => {
if ("fieldOne" in value) {
return "ObjectOne";
Expand All @@ -64,7 +64,7 @@ describe("Unions", () => {

const UnionWithClassResolveType = createUnionType({
name: "UnionWithClassResolveType",
types: [ObjectOne, ObjectTwo],
types: () => [ObjectOne, ObjectTwo],
resolveType: value => {
if ("fieldOne" in value) {
return ObjectOne;
Expand Down Expand Up @@ -311,6 +311,30 @@ describe("Unions", () => {
});
});

describe("typings", () => {
it("should correctly transform to TS union type when using extending classes", async () => {
getMetadataStorage().clear();
@ObjectType()
class Base {
@Field()
base: string;
}
@ObjectType()
class Extended extends Base {
@Field()
extended: string;
}
const ExtendedBase = createUnionType({
name: "ExtendedBase",
types: () => [Base, Extended] as const,
});
const _extended: typeof ExtendedBase = {
base: "base",
extended: "extended",
};
});
});

describe("Mutliple schemas", () => {
it("should correctly return data from union query for all schemas that uses the same union", async () => {
getMetadataStorage().clear();
Expand Down

0 comments on commit 2e5f155

Please sign in to comment.