relay-tools / relay-compiler-language-typescript

⛔️ Obsolete - A language plugin for Relay that adds TypeScript support, including emitting type definitions.
MIT License
241 stars 70 forks source link

%future added value #203

Open renanmav opened 4 years ago

renanmav commented 4 years ago

Why do we have this?

https://github.com/relay-tools/relay-compiler-language-typescript/blob/4134988f48d8522cc2403c2da748a428ddebeea5/src/TypeScriptGenerator.ts#L1061-L1063

kassens commented 4 years ago

This was copied from the Flow type definitions, I think the same applies to TypeScript though:

If you used Relay as part of React Native for example, you might have some client deployed and don't want to force-update that client.

At least Flow supports "exhaustiveness checks" where the type checker knows that one branch of a switch will match.

If your server now adds another enum value in the future (this is not considered a breaking change per GraphQL spec) the old client would somehow have to deal with this newly added value. This value is reflecting this presumed value added in the future do force you to add a default case to the switch statement or similar.

0x450x6c commented 3 years ago

If you used Relay as part of React Native for example, you might have some client deployed and don't want to force-update that client.

Fair point, but when we provide default case - we lose the exhaustive checks, compiler will not notify us when schema changes.

Probably, better approach is to keep these types, and change enum value to the %future added value in case of unexpected values.

0x450x6c commented 3 years ago

I am using following helper function for ts-pattern:

import * as tsPattern from "ts-pattern";
import { typeFest } from "./type-fest";

// eslint-disable-next-line
export type RelayFutureAddedValue = "%other" | "%future added value";

export type UnionToTuple<T> = typeFest.UnionToIntersection<
  T extends never ? never : (t: T) => T
> extends (_: never) => infer W
  ? readonly [...UnionToTuple<Exclude<T, W>>, W]
  : readonly [];

/**
 * Matches unexpected value by union type.
 *
 * This way, value that does not match `possibleValues` works as default branch.
 *
 * It is useful with relay's `%other` and `%future added value` (see https://github.com/relay-tools/relay-compiler-language-typescript/issues/203#issuecomment-698443972).
 */
export const whenUnexpectedUnion = <T extends string>(
  possibleValues: UnionToTuple<Exclude<T, RelayFutureAddedValue>>,
) =>
  tsPattern.when(
    (value: string): value is RelayFutureAddedValue =>
      !(possibleValues as readonly string[]).includes(value),
  );

Example usage of helper function in react app:

tsPattern
  .match(error)
  .with(whenUnexpectedUnion<typeof error>(["WRONG_CREDENTIALS"]), () => (
    <div key="unexpected" className={styles.generalError}>Something went wrong</div>
  ))
  .with("WRONG_CREDENTIALS", (error) => (
    <div key={error} className={styles.generalError}>
      Wrong credentials.
    </div>
  ))
  .exhaustive()

This way, unexpected value handled correctly without providing default branch, so typescript will inform us when schema changed.