Sam Denty
Sam Denty

Reputation: 4095

Typescript generics + conditional types is not assignable to type error

I'm using conditional types to automatically infer the type of a data structure through class generics. For some reason it's not inferring the types in the ObjectType constructor.

Typescript playground

   export type NodeType<T> = T extends (infer U)[] ? ArrayNode<U> : ObjectNode<T>

export abstract class Node {}
export class ObjectNode<T> extends Node {
  constructor(public fields: { [key in keyof T]: Field<NodeType<T[key]>> }) {
    super()
  }
  public data: T
}
export class ArrayNode<T> extends Node {
  public data: T[]

  constructor(public ofType: NodeType<T>) {
    super()
  }
}

class Field<T extends Node> {
  constructor(public node: T) {}
}

const User = new ObjectNode({})

const query = new ObjectNode({
  user: new Field(User),
  // ****************
  /// The below `users` field should be automatically be detected as 'Field<ArrayNode<{}>>', but for some reason it's 'Field<ObjectNode<{}>>'.

  /// Property 'fields' is missing in type 'ArrayNode<{}>' but required in type 'ObjectNode<{}>'.
  // ****************
  users: new Field(new ArrayNode(User))
})

var q: ObjectNode<{ users: {}; user: {} }>

q.fields.users.node.fields
q.fields.user.node.fields
q.data.user
q.data.users

query.fields.users.node.fields
query.fields.user.node.fields
query.data.user
query.data.users

Upvotes: 1

Views: 572

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250396

I don't think typescript can follow the logic of the conditional type to extract T. You need to reverse the logic and take in the node type as the type parameter and manually etxract the data type from the node type. A solution could look something like this:

export abstract class Node { }
type AllNodeTypes = ArrayNode<any> | ObjectNode<any>

export type NodeDataType<T extends AllNodeTypes> = T['data']; // can be a conoditional type with infer if needed (ie if not all node types have a data field)
export type ObjectNodeDataType<T extends Record<keyof T, Field<AllNodeTypes>>> = {
  [P in keyof T]:NodeDataType<T[P]['node']>
} 
export class ObjectNode<TNode extends Record<keyof TNode, Field<AllNodeTypes>>> extends Node {
  constructor(public fields: TNode) {
    super()
  }
  public data: ObjectNodeDataType<TNode>
}
export class ArrayNode<TNode extends AllNodeTypes> extends Node {
  public data: NodeDataType<TNode>[]

  constructor(public ofType: TNode) {
    super()
  }
}

class Field<T extends Node> {
  constructor(public node: T) { }
}

const User = new ObjectNode({})

const query = new ObjectNode({
  user: new Field(User),
  users: new Field(new ArrayNode(User))
})

query.fields.users.node.ofType.fields
query.fields.user.node.fields
query.data.user
query.data.users

Upvotes: 3

Related Questions