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

How to use the Extensions Row type machinery when using a type with a custom parser. #2080

Closed
thruflo opened this issue Dec 2, 2024 · 3 comments

Comments

@thruflo
Copy link
Contributor

thruflo commented Dec 2, 2024

I have a type like this:

type Todo = {
  id: string
  title: string
  completed: boolean
  created_at: Date
}

The created_at field is a Date column, stored as a timestamptz field in Postgres. This is not one of the types supported by the default parser, so I use a custom parser with my ShapeStreamOptions:

const stream = new ShapeStream<Todo>({
  url: `${ ELECTRIC_URL }/v1/shape`,
  table: 'todos',
  parser: {
    timestamptz: (value: string) => new Date(value)
  }
})

I later call stream.subscribe with a callback function and get a type error. Just to give the full context, I call stream.subscribe as part of this matchStream function:

export function matchStream<T extends Row>(
  stream: ShapeStreamInterface<T>,
  operations: Array<Operation>,
  matchFn: (message: ChangeMessage<T>) => boolean,
  timeout = 60000 // ms
): Promise<ChangeMessage<T>> {
  return new Promise<ChangeMessage<T>>((resolve, reject) => {
    const unsubscribe: () => void = stream.subscribe((messages) => {
      const message = messages.filter(isChangeMessage).find((message) => {
        const operation = message.headers.operation

        return operations.includes(operation) && matchFn(message)
      })
      // ...

E.g. by calling:

matchStream(stream, ['delete'], matchBy('id', id))

This raises a type error:

patterns/2-optimistic-state/index.tsx:156:9 - error TS2345: Argument of type 'ShapeStream<Todo>' is not assignable to parameter of type 'ShapeStreamInterface<Row<never>>'.
  Types of property 'subscribe' are incompatible.
    Type '(callback: (messages: Message<Todo>[]) => MaybePromise<void>, onError?: ((error: FetchError | Error) => void) | undefined) => () => void' is not assignable to type '(callback: (messages: Message<Row<never>>[]) => MaybePromise<void>, onError?: ((error: FetchError | Error) => void) | undefined) => () => void'.
      Types of parameters 'callback' and 'callback' are incompatible.
        Types of parameters 'messages' and 'messages' are incompatible.
          Type 'Message<Todo>[]' is not assignable to type 'Message<Row<never>>[]'.
            Type 'Message<Todo>' is not assignable to type 'Message<Row<never>>'.
              Type 'ChangeMessage<Todo>' is not assignable to type 'Message<Row<never>>'.
                Type 'ChangeMessage<Todo>' is not assignable to type 'ChangeMessage<Row<never>>'.
                  Type 'Todo' is not assignable to type 'Row<never>'.
                    Property 'created_at' is incompatible with index signature.
                      Type 'Date' is not assignable to type 'Value<never>'.
                        Type 'Date' is not assignable to type '{ [key: string]: Value<never>; }'.
                          Index signature for type 'string' is missing in type 'Date'.

Now, I see there's some clever type machinery in the Row type definition in the Typescript client in src/types.ts designed to support types using a custom parser:

/**
 * Default types for SQL but can be extended with additional types when using a custom parser.
 * @typeParam Extensions - Additional value types.
 */
export type Value<Extensions = never> =
  | string
  | number
  | boolean
  | bigint
  | null
  | Extensions
  | Value<Extensions>[]
  | { [key: string]: Value<Extensions> }

export type Row<Extensions = never> = Record<string, Value<Extensions>>

export type GetExtensions<T extends Row<unknown>> =
  T extends Row<infer Extensions> ? Extensions : never

How do I use this when defining my Todo type to provide the information about the created_at field's Date type?

@kevin-dp
Copy link
Contributor

kevin-dp commented Dec 3, 2024

@thruflo I explained usage of never vs unknown in this context here: #1791 (comment)

We use never as the default when we want to ensure that if no type argument is passed, we treat it as only the base types with no extensions. And we use unknown whenever we want to allow the user to pass in any extended type.

If we look at your snippet:

export function matchStream<T extends Row>

The problem is that T is said to extend Row.
Since you don't pass any type argument to Row it uses the default of never, so in fact we have:

export function matchStream<T extends Row<never>>

Thus, we require T to extend Row and we don't allow any extensions since the extensions are set to never.
But, matchStream(stream, ['delete'], matchBy('id', id)) is calling it with Todo as a type argument which does define an extension. Hence, the type mismatch.

To fix this problem, you need to modify your definition of matchStream to allow unknown extensions:

export function matchStream<T extends Row<unknown>>

Now, we're saying that T can be a row type and may include extensions (whose type we actually don't know).

@thruflo
Copy link
Contributor Author

thruflo commented Dec 3, 2024

Brilliant, thanks for the lesson :)

Out of interest, I see GetExtensions used in the type signatures of a few of the react-hooks functions, like getShapeStream. Is there any scenario where I might use it directly?

@kevin-dp
Copy link
Contributor

kevin-dp commented Dec 3, 2024

Out of interest, I see GetExtensions used in the type signatures of a few of the react-hooks functions, like getShapeStream. Is there any scenario where I might use it directly?

You can use it whenever you need the type of the extensions but you only have the entire Row type.
So, GetExtensions<Row<Todo>> = Todo but you would use it when you need to get the extensions type from some type T where T extends Row<unknown>.

@kevin-dp kevin-dp closed this as completed Dec 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants