Typescript constructor example?
renzor-fist opened this issue · 6 comments
Hi there, love the library so far but am struggling with a basic use case in Typescript and am hoping you could provide guidance to help beef up the typescript example section.
I have a HashMismatchError that receives two props
.
throw new HashMismatchError(
`Hash mismatch: expected ${stored.hashValue} but got ${foundHashValue}`,
{
props: {
foundHash: foundHashValue,
storedHash: stored.hashValue,
},
},
)
Its defined like this:
export const HashMismatchError = ModernError.subclass("HashMismatchError", {
custom: class extends ModernError {
foundHash: string
storedHash: string
constructor(
message: string,
options: InstanceOptions<Plugins> | undefined,
) {
super(message, options)
// @ts-expect-error can't figure out how to type this
this.foundHash = options?.props?.foundHash
// @ts-expect-error can't figure out how to type this
this.storedHash = options?.props?.storedHash
}
},
})
Could you provide any guidance on how to clean up the constructor function signature and hash variable assignments? It would help me and others understand how to use constructor functions with the library. Thanks for your awesome lib!
Hi @renzor-fist,
Thanks for reaching out. I'm glad you find the library useful!
The main issue is that props
is a built-in feature with its own semantics. When adding custom constructor options, I would advise making them separate attributes, as opposed to setting them inside props.*
, to avoid conflicting with props
built-in behavior and types.
In other words, something along the following lines:
export const HashMismatchError = ModernError.subclass("HashMismatchError", {
custom: class extends ModernError {
foundHash: string | undefined
storedHash: string | undefined
constructor(
message: string,
options?: InstanceOptions & { foundHash?: string, storedHash?: string }
) {
super(message, options)
this.foundHash = options?.foundHash
this.storedHash = options?.storedHash
}
},
})
In your case though, you do not need to set this.foundHash = ...
in the constructor, because props
are already automatically set on any error instance and correctly typed. I am guessing this might be because you simplified your example for the sake of illustration, is that correct? Maybe your original use case actually transforms the option values before setting them?
Thanks for the quick, detailed response @ehmicky . Maybe I'm using the library the wrong way. I have a function that throws
throw new HashMismatchError(
`Expected x but got y`,
{
props: {
foundHash,
storedHash
},
},
)
Elsewhere, I'm trying to read err.storedHash
from the error, but it's not typed as I would expect (even though its defined at runtime).
const sendHashMismatchMessage = (
error: typeof HashMismatchError,
) => {
if (error.foundHash) { // Property 'hashType' does not exist on type 'ErrorSubclassCore<[], {}, typeof custom>'
// do something
}
}
It sounds like you're saying I should simply be instantiating HashMismatchError
instead of using props like:
new HashMismatchError(`Expected x but got y`, foundHash, storedHash)
Is this correct?
Thanks for the additional information. There are several possible situations, let me explain each.
- When calling
new HashMismatchError('...', { props })
, the error instanceprops
are correctly typed. This only applies to that specific error instance.
- When a given error class is expected to be instantiated with always the same
props
, and you want to use the type of any instance of it (as opposed to a specific one). For example, yoursendHashMismatchMessage()
expects any instances ofHashMismatchError
, which are always instantiated withfoundHash
.
The simplest way to do this is to pass props
as an option when creating the error class.
export const HashMismatchError = ModernError.subclass("HashMismatchError", {
props: { foundHash: '', storedHash: '' },
})
type FoundHash = InstanceType<typeof HashMismatchError>["foundHash"] // This is correctly `string`
This assigns default values for those props
with that given error class. It also types them.
If you just want to type the props
but not assign any default values, you can use a type assertion on an empty object.
export const HashMismatchError = ModernError.subclass("HashMismatchError", {
props: {} as { foundHash: string, storedHash: string },
})
In either case, you can use a custom
constructor if you want to perform some validation of those props
, such as ensuring they are passed by users.
interface HashProp {
foundHash: string
storedHash: string
}
export const HashMismatchError = ModernError.subclass('HashMismatchError', {
props: {} as HashProp,
custom: class extends ModernError {
constructor(
message: string,
options?: InstanceOptions & { props?: Partial<HashProp> },
) {
super(message, options)
if (typeof options?.props?.foundHash !== 'string') {
throw new TypeError('Please specify a "foundHash" option.')
}
}
},
})
- If you want to do anything more complex, such as transforming the value of those
props
, you should then use custom constructoroptions
, as shown in my example above.
This would result in something like this. Please note those are passed as part of the error constructor's second argument, which is an object. This is slightly different from your example, which used two positional arguments instead. Modern-errors' constructors always take the following shape: new Error('message', options)
because this is the standard JavaScript's shape. Changing that shape would also conflict with some built-in features: for example, this would prevent users from being able to pass options.props
.
So when adding custom options, one can either add them to the constructor's second argument (which is an object), or add a third positional argument. Either way, those must be optional.
new HashMismatchError(`Expected x but got y`, { foundHash, storedHash })
Please let me know what you think! :)
Outstanding. Thank you sir. You have solved all of my problems.
- I was unaware of the
InstanceType
utility which was a big missing piece. - Both of those examples involving default and validated props are excellent and would improve the documented examples if they're not already there.
Thanks for your awesome library and help. Cheers sir.
Good suggestion! I have just improved the following documentation based on our discussion:
- How to type
props
: https://github.com/ehmicky/modern-errors/blob/main/docs/typescript.md#error-properties-and-methods - How to implement custom options: https://github.com/ehmicky/modern-errors#-custom-logic
- How to type custom options: https://github.com/ehmicky/modern-errors/blob/main/docs/typescript.md#custom-options
- How to use error classes' and instances' type: https://github.com/ehmicky/modern-errors/blob/main/docs/typescript.md#type-inference
Please let me know what you think!
@all-contributors Could you please add @renzor-fist for question and ideas?
I've put up a pull request to add @renzor-fist! 🎉