-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Type errors declaring a new language definition in TypeScript #3272
Type errors declaring a new language definition in TypeScript #3272
Comments
I don't know why it added the I have a further question about the |
Let's keep them separate. |
I think this is a result of how I was thinking about types/interfaces as requirements, as in how they force your code to conform to the type/interface. In this case accepting a
Yep.
I'm going to get one other person's thoughts on this but at first glance this is quite annoying indeed and it does seem we should change this to specify that the parameter is required rather than optional. |
That's actually interestingly not the case in TypeScript! type MyFunction = () => {}
type MyOptionalFunction = (arg?: string) => {}
type MyArgFunction = (arg: string) => {} Consider these three function types/signatures. See this playground // This is fine
const test1: MyFunction = () => {}
// This is fine
const test2: MyOptionalFunction = () => {}
// This is fine
const test3: MyArgFunction = () => {}
// This is fine because the arguments are optional, so it can
// be undefined. () is the same as (arg: string | undefined)
// and always passing in undefined.
const test4: MyFunction = (arg?: string) => {}
// This is not fine, the argument MUST be optional, otherwise
// this will break when it's not given or when undefined is
// passed in.
//
// @ts-expect-error
const test5: MyOptionalFunction = (arg: string) => {}
// This is fine because arg?: string is WIDER than arg: string
const test6: MyArgFunction = (arg?: string) => {}
// This is fine because string | number is WIDER than string
const test7: MyArgFunction = (arg: string | number) => {}
// Fine because it expects no args
test1()
// Not fine because it expects no args
//
// @ts-expect-error
test1('fine')
// Fine because it's optional
test2()
// Fine because it's allowed, despited test2 not using it
test2('fine')
// Not fine, despite test3 not using it, because it's marked
// as required.
//
// @ts-expect-error
test3()
// Fine
test3('fine')
// Fine
test4()
// Not fine, despite test4 allowing it in its anonymous
// definition. MyFunction doesn't allow it, therefore it's
// not allowed
//
// @ts-expect-error
test4('fine') So when you write: type LanguageFn = (hljs?: HLJSApi) => Language It means:
In the same way when you write: type LanguageFn2 = (hljs: HLJSApi) => Language It means:
@jsharkey13 was right that because it's always called with the argument, the argument shouldn't be optional. However, James, for future reference, if you require the argument to produce a sane response, the right way to handle that is: const myLanguageRules: LanguageFn = function(hljs) {
if (!hljs) {
throw new Error('HLJSApi is required for this language')
}
// Here your type is narrowed to HLJSApi without undefined
} It's not hacky to require to narrow the type if the argument is truly optional. |
Ah, but of course - and thanks for detailed examples. I think I was maybe confusing arguments with keys on object literals (where TS forces your hand much more). That's just a guess on why I went this way originally. So it seems the PR in question should resolve this then. |
@SleeplessByte - ah, thank you! (And neat example cases too!) I agree the PR will fix it. |
Fall back to substituting a plaintext grammar and log an error in production mode. (or blow up in debug mode)
Yes, quite convenient. :) |
Describe the issue/behavior that seems buggy
I am attempting to declare and register a new language definition for an internal pseudolanguage (aimed at students, and very simplistic), but doing so in TypeScript and not plain JS. I am using
11.1.0
.I declare the new rules in a function I want to pass into
hljs.registerLanguage(...)
using theLanguageFn
type you export fromindex.d.ts
. HoweverLanguageFn
annotates thehljs
argument as optional, which means that I cannot use something likehljs.QUOTE_STRING_MODE
in my function without a guard againsthljs
being undefined as the type annotation says it might be.Sample Code or Instructions to Reproduce
A minimal TypeScript example would be:
where the references to
hljs.QUOTE_STRING_MODE
and number mode will cause type errors sincehljs
has the typeHLJSApi | undefined
(the error on trying to usehljs
isTS2532: Object is possibly 'undefined'.
). This is such a simple case that you can see it will cause a problem without even needing to use the compiler.The only way to do this guard in a way that allows me to use
hljs
in many places in my definition seems to be the incredibly hackyif (!hljs) return {} as Language;
at the start, otherwise the type system complains about the return type of the function being incorrect. However I don't understand whyhljs
is marked as optional in the definitionexport type LanguageFn = (hljs?: HLJSApi) => Language
in the first place? Surely it is always passed in by the library?Expected behavior
I would expect the
LanguageFn
type to actually beexport type LanguageFn = (hljs: HLJSApi) => Language
without the argument being optional, and then if I choose not to use thehljs
argument I can just name it_hljs
in my own function and TypeScript knows this means I do not intend to use the argument.I could foresee changing this signature causing issues for anyone using TypeScript to define languages of their own if (and probably only if) they do not have a
hljs
argument at all in their function. Otherwise it seems safe to change. Was there a reason it is declared the way it is? Is it autogenerated, or written by hand?The text was updated successfully, but these errors were encountered: