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

No IntelliSense with const assertions #41247

Closed
4 tasks done
cyrilletuzi opened this issue Oct 26, 2020 · 12 comments
Closed
4 tasks done

No IntelliSense with const assertions #41247

cyrilletuzi opened this issue Oct 26, 2020 · 12 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@cyrilletuzi
Copy link

cyrilletuzi commented Oct 26, 2020

Search Terms

const assertion

Suggestion

interface JSONSchemaString {
  type: 'string';
  enum: readonly string[];
}

interface JSONSchemaNumber {
  type: 'number';
}

type JSONSchema = JSONSchemaString | JSONSchemaNumber;

const schema: JSONSchema = {
  type: 'string';
  enum: ['hello', 'world'];
} as const;

Solutions:

  • schema should keep the exact type inferred by as const, and not be widened back to JSONSchema,
  • or there should be another way to assert as const while using a type to enable autocompletion.

Use Cases

My library (>15K weekly downloads) manages client-side storage, where data is validated at runtime via a JSON schema, so the API is basically:

this.storage.get('key', schema);
this.storage.set('key', value, schema);

Given the nature of JSON Schema structure, simplified in the example above, some cases require as const assertion to correctly infer types (for example the enum values).

When doing as below, autocompletion is available for the schema:

this.storage.get('key', { type: 'string'; enum: ['hello', 'world']; } as const);

But as the same schema must be used with .set(), a variable is required to store it, and now there will be no autocompletion at all (which is normal):

const schema = { type: 'string'; enum: ['hello', 'world']; } as const;

Unfortunately, trying to resolve this scenario by adding an explicit type adds back autocompletion, but also widen back the schema type (as if there was no as const assertion):

const schema: JSONSchema = { type: 'string'; enum: ['hello', 'world']; } as const;

So currently, I'm forced to lose autocompletion if I want to implement this feature: cyrilletuzi/angular-async-local-storage#477

Checklist

My suggestion meets these guidelines:

  • [don't know] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RebeccaStevens
Copy link

schema should keep the exact type inferred by as const, and not be widened back to JSONSchema

That would be a breaking change.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Nov 4, 2020
@RyanCavanaugh
Copy link
Member

It seems like you need #7481. Changing type annotations as suggested would be a cataclysmic breaking change; that's not happening.

@cyrilletuzi
Copy link
Author

@RyanCavanaugh Thanks for your answer. It's not clear to me how #7481 would solve my issue, but I believe you.

Changing type annotations as suggested would be a cataclysmic breaking change; that's not happening.

Note it is just one possible solution I suggested (being very unaware of compiler details). But the real issue is that const assertions in general don't have any IntelliSense. A new non-breaking feature/syntax/whatever would be very OK to me.

@cyrilletuzi
Copy link
Author

However I don't agree with the "Working as intended" flag. That the current syntax works like this and can't change, OK, but that const assertions in general don't have any IntelliSense is still an issue and would need a solution.

@cyrilletuzi cyrilletuzi changed the title Const assertion with explicit type: don't widen back or allow autocompletion No IntelliSense with const assertions Nov 4, 2020
@cyrilletuzi
Copy link
Author

Something like as const<JSONSchema> or as <JSONSchema>const?

@RyanCavanaugh
Copy link
Member

but that const assertions in general don't have any IntelliSense is still an issue and would need a solution.

The type is the type; what you're proposing would be that IntelliSense would operate on something other than the type -- some side channel -- but that would be very out of scope.

#7481 is what because you're basically trying to verify that the expression matches JSONSchema while still retaining the expression type as the type of schema

@cyrilletuzi
Copy link
Author

cyrilletuzi commented Nov 4, 2020

@RyanCavanaugh

The type is the type; what you're proposing would be that IntelliSense would operate on something other than the type -- some side channel -- but that would be very out of scope.

When doing this:

this.storage.get('key', { type: 'string'; enum: ['hello', 'world']; } as const);

given that the function signature is:

get(key: string, schema: JSONSchema) {}

we do have both IntelliSense and the type asserted as const. So it doesn't seem so out of scope to me, I would just need be able to have the same behavior/feature when declaring a variable and not just when declaring a function.

I already thought about the workaround solution mentioned in #7481, ie. having an utility function like this:

function asType<T extends JSONSchema>(value: T): T {
  return value;
}

Issue is: it doesn't work with const assertions, as:

const schema = asType({ type: 'string' }) as const;

is not allowed and errors with:

A 'const' assertions can only be applied to references to enum members, or string, number, boolean, array, or object literals.ts(1355)

@RebeccaStevens
Copy link

@cyrilletuzi

Is this the function definition you are talking about?

If so I think something like this is more what you would be wanting to do:

get<T extends JSONSchema>(key: string, schema: T): ...

If you want I can probably help you come up with the right typing for you're case. If you could provide a minimal playground or sandbox that declares the function and a few examples of how you want the way you want to call it that demonstrate the issue you are having, I'd be happy to help.

@cyrilletuzi
Copy link
Author

@RebeccaStevens Thanks.

The function definition you quoted is the current one, which is a mess of overloads because inferring the TS type from a JSON schema was not possible before.

The new one is here, and it is indeed like the example you gave:

get<Schema extends JSONSchema>(key: string, schema: Schema) {
   return ... as InferFromJSONSchema<Schema>;
}

It is strongly related to the InferFromJsonSchema type here (note that this type will be simplified once TS 4.1 is available, thanks to recursive conditional types).

For InferFromJsonSchema to work properly (for example to infer the JSON schema required properties of an object, or the possible values of a JSON schema enum), the JSON schema passed by the lib user must be asserted as const. Otherwise the type of enum and required is just string.

Which leads to the situation I explained in the first message of this issue, ie. doing this:

this.storage.get('key', { type: 'string' } as const);

works and provides autocompletion.

But as the same JSON schema must be reused in the .set() method, the schema must be stored in a variable. Then doing this:

const schema = { type: 'string' } as const;
this.storage.set('key', schema);
this.storage.get('key', schema);

works but do not provide autocompletion when writing the JSON schema, while:

const schema: JSONSchema = { type: 'string' } as const;
this.storage.set('key', schema);
this.storage.get('key', schema);

provides autocompletion but fails, as schema is not asserted as const anymore, but widened back to JSONSchema type.

@RebeccaStevens
Copy link

I have an architectural question. Why does the schema need to be provided to set and get? Why not just require that the schema is set when the the storage object is initially created? i.e. new StorageMap(..., schema).

Back to the current issue at hand. What is the autocompletion supposed to take place on? and what fails?
I made this codesandbox to play around with things.

@cyrilletuzi
Copy link
Author

cyrilletuzi commented Nov 4, 2020

I have an architectural question. Why does the schema need to be provided to set and get? Why not just require that the schema is set when the the storage object is initially created? i.e. new StorageMap(..., schema).

The schema is not global to the whole storage, there is one schema for each key. I am thinking of a new API where all possible keys and schemas would be prepared in advance, but that may be even more challenging on the TypeScript side (because the lib user would need to pass the whole config object to Angular, and I would need to infer the TS types of all this config without requiring from the lib user to describe the whole config again in TS types).

What is the autocompletion supposed to take place on?

In your codesandbox, try to add enum in schema1 object > autocomplete. Try the same in schema2 object > no autocomplete. And autocomplete is quite important here for DX, as not everyone is familiar with all the JSON schemas options, and available JSON schemas options are different depending on the type.

what fails?

When schema1 (ie. no as const assertion) will be passed to the InferFromJsonSchema type, inference will be inaccurate for things that need to be literals and not just string.

For example with the JSON schema enum option:

const schema2 = { type: 'string', enum: ['hello', 'world'] } as const;

passed to InferFromJsonSchema will infer 'hello' | 'world' type.

While:

const schema1: JSONSchema = { type: 'string', enum: ['hello', 'world'] };

passed to InferFromJsonSchema will infer string type (because enum itself have been widened to string in schema1 type).

I gave the enum example because it is simpler to explain, but one could consider string inference is still acceptable while not the best. But the same goes for required JSON schema option, which tell which properties are required in an object, and for this one it completely messes up the inference.

I'll do a sandbox tomorrow if my explanations are not enough.

@cyrilletuzi
Copy link
Author

After a proof of concept, I will be able to avoid this issue by unrelated changes in my lib.

While I think it is a real issue that I won't be alone to face, and that it will be more and more common (as as const assertions are more and more often required in conjunction with new advanced TS features introduced in last versions, like conditional types), I will close it as it has been marked as "Working as intended", which means the issue is dead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

3 participants