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

Allow usage for indexed types on body elements #1034

Open
mahirk opened this issue Sep 25, 2020 · 11 comments
Open

Allow usage for indexed types on body elements #1034

mahirk opened this issue Sep 25, 2020 · 11 comments
Labels
enhancement New feature or request question Further information is requested

Comments

@mahirk
Copy link
Contributor

mahirk commented Sep 25, 2020

Is your feature request related to a problem? Please describe.
The JSON specification allows an indexed type by defining it as an object which can have any number of strings in it, for example:
the following type definition:

metadata?: {
        [key: string]: string;
    };

Becomes:

        "metadata": {
          "additionalProperties": {
            "type": "string"
          },
          "type": "object"
        },

Describe the solution you'd like
The following spec fails:

class Api {}

@endpoint({
  method: "POST",
  path: "/users/:someOtherString"
})
class CreateUser {
  @request
  request(
    @headers
    headers: HeaderAlias,
    @pathParams
    pathParams: PathParams,
    @body body: CreateUserRequest
  ) {}

  @response({ status: 201 })
  successResponse(@body body: CreateUserResponse) {}
}


interface CreateUserResponse {
  firstName: string;
  lastName: string;
  role: string;
  [key: string]: string
}

with the error as indexed types are not allowed.

The proposal would be to update the type-parser to allow indexed types and define them using the guidelines provided above.

@mahirk
Copy link
Contributor Author

mahirk commented Sep 25, 2020

@lfportal Happy to work on it, and get any implementation ideas you may have on the same. Any motivation on why it was initially not permitted would be beneficial!

@mahirk
Copy link
Contributor Author

mahirk commented Sep 25, 2020

I also noticed the following which capability matrix:
union type -> ✅
inheritance type -> ✅
recursive -> 🔴
generics -> 🔴

Would love to hear any thoughts you have on implementing these.

@lfportal
Copy link
Contributor

Any motivation on why it was initially not permitted would be beneficial!

This was considered, but was not a priority use case for us. Ultimately we deemed TypeScript's indexed types not flexible enough to support this feature in a sufficiently generic way. Where an indexed type is present, defined sibling properties must also be assignable to the index type definition:

interface CreateUserResponse {
  firstName: string; // Property 'firstName' of type 'string' is not assignable to string index type 'boolean'.
  [key: string]: boolean;
}

@lfportal
Copy link
Contributor

lfportal commented Sep 26, 2020

recursive -> 🔴

The type parser would need to be modified to pass around some sort "type parse stack" structure to keep track of types currently being parsed as it traverses the types to short circuit reference type parsing. I'm not too familiar with recursive type representation with the OpenAPI spec. Are you aware of any particular limitations @mahirk?

generics -> 🔴

interface Generic<T> {
  genericProp: T;
}

type A = Generic<String>

Off the top of my head:

  1. Parse type parameters (T)
  2. Parse the generic interface, passing in the resolved type parameters

Questions that need answering:

  • Do we store the resulting type in the typeTable?
    • I'm thinking we don't, and treat it as if it were defined inline (i.e. type A = { genericProp: String; }) to keep it simple
    • If we do, the need some way to name the type. GenericString, GenericInt? But this could lead to type name clashes

Thoughts @mahirk?

@lfportal lfportal added enhancement New feature or request question Further information is requested labels Sep 26, 2020
@mahirk
Copy link
Contributor Author

mahirk commented Oct 6, 2020

Apologies, @lfportal got pulled off to something else:

Ultimately we deemed TypeScript's indexed types not flexible enough to support this feature in a sufficiently generic way

Absolutely, I am curious though, what about scenarios which could enable an open-ended pair of single level JSONs? Although I do understand the type safety aspect of it, I was thinking more along the lines of the metadata which is available here: https://stripe.com/docs/api/metadata. You can see the definition here: https://raw.github.com/stripe/openapi/master/openapi/spec3.yaml

metadata:
          additionalProperties:
            maxLength: 500
            type: string
          description: Set of [key-value pairs](https://stripe.com/docs/api/metadata)
            that you can attach to an object. This can be useful for storing additional
            information about the object in a structured format.
          type: object

recursive -> 🔴 ....Are you aware of any particular limitations @mahirk?

Swagger UI seems to have trouble, and potentially the model generation might have issues. Let me check those and then evaluate if it would be okay. As far as representation, that's possible using the specification swagger-api/swagger-ui#3325

Questions that need answering

I agree with that, it may be something the system calls out to the developers. I don't forsee any issues with generating the type in that way however.

@lfportal
Copy link
Contributor

lfportal commented Oct 8, 2020

Absolutely, I am curious though, what about scenarios which could enable an open-ended pair of single level JSONs?

Do you mean to allow index signatures only where no sibling properties are defined? i.e:

// allowed
interface A { 
  [key: string]: string;
}

// not allowed
interface B {
  [key: string]: string;
  a: string
}

@mahirk
Copy link
Contributor Author

mahirk commented Oct 9, 2020

Yup @lfportal i.e. if it has sibling properties, then spot can log an error stating that sibling properties on index types are not permitted. That should satisfy the use case and maybe also force better design of open-ended structs?

@lfportal
Copy link
Contributor

I'm thinking a simpler way to constrain the usage could be TypeScript's Record<string, *> type. The parser would only need to constrain the first type argument to string - disallowing number and literals "a" | "b" | "c".

@Noeyfan
Copy link

Noeyfan commented Oct 21, 2020

Hi @mahirk @lfportal

Catching up with the discussion.

generics -> 🔴

interface Generic<T> {
  genericProp: T;
}

type A = Generic<String>

Off the top of my head:

  1. Parse type parameters (T)
  2. Parse the generic interface, passing in the resolved type parameters

Questions that need answering:

  • Do we store the resulting type in the typeTable?

Just to confirm we will store Generic in the typeTable regardless of we storing the resulting types, right?

  • I'm thinking we don't, and treat it as if it were defined inline (i.e. type A = { genericProp: String; }) to keep it simple
  • If we do, the need some way to name the type. GenericString, GenericInt? But this could lead to type name clashes

inline type works great when the Generic object is small, but when its big and frequently referenced, the cost of expanding it every where goes up very quickly. The name clashes should not be a problem if we just use the full name Generic<String>. Maybe we can have a threshold on when we should put it into typeTable?

Also, are we only considering handle custom generic type, or we will also support typescript utility types? https://www.typescriptlang.org/docs/handbook/utility-types.html

As a side note on typescript supported types, I noticed: undefined, object, unknown are giving unknown type error at the moment, is it expected?

generics/recursive/index-type/util-type It feels like we should create few separate issue to address each of the problem and discuss them separately? thoughts?

@lfportal
Copy link
Contributor

inline type works great when the Generic object is small, but when its big and frequently referenced, the cost of expanding it every where goes up very quickly. The name clashes should not be a problem if we just use the full name Generic<String>

The type table is used to generate the components section of OpenAPI documents. We would require some strategy to convert Generic<String> into something OpenAPI friendly. Whatever that becomes will be subject to name clashing with user defined types (this will be true for typescript's utility types too). I think we should aim to avoid any implicit naming, and stick with a clear and predictable syntax. Currently the type table only stores types declared using type aliases and interfaces. I don't see any need to complicate this:

body: {
  a: Generic<String>; // inline type, rendered as an inline schema in OpenAPI
  b: MyType; // reference type, rendered as a $ref schema in OpenAPI
}

type MyType = Generic<String> // stored in type table

As a side note on typescript supported types, I noticed: undefined, object, unknown are giving unknown type error at the moment, is it expected?

This is currently expected. However, undefined is indirectly supported using the question token to indicate optionality myOptionalField?: string. Do you have a use case for using these types? Perhaps we can discuss them in a separate issue/s.

generics/recursive/index-type/util-type It feels like we should create few separate issue to address each of the problem and discuss them separately? thoughts?

Yup, agreed, I'll leave this issue to focus on indexed types.

@jankuca
Copy link
Contributor

jankuca commented Nov 26, 2020

This feature would be really useful for us as well. Allowing indexed types without explicitly listed siblings is more than enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants