We're working on converting a project from Typescript (.ts
) to Typescript-via-JSDOC (.js
).
We have a class StateHolder
, instances of which contain data described by the generic State
. The data can be provided at instantiation, or later on.
Here's a very minimal version of how it looks in Typescript, which passes type-checking and compiles without issue:
// state.ts
type StateHolderOptions = {};
class StateHolder<State> {
value?: State;
constructor(
initialValue?: State,
options: StateHolderOptions = {}
) {
if (initialValue !== undefined) {
this.value = initialValue;
}
}
}
type MyState = { data: string };
const state = new StateHolder<MyState>(undefined, {});
Here's the same code as we've converted it into JS:
// state.js
/**
* @typedef {object} StateHolderOptions
*/
/**
* @template State
*/
class StateHolder {
/**
* @type {State | undefined}
*/
value;
/**
* @param {State} [initialValue]
* @param {StateHolderOptions} [options] - Unused
*/
constructor(initialValue, options = {}) {
if (initialValue !== undefined) {
this.value = initialValue;
}
}
}
/**
* @typedef {{ data: string }} MyState
*/
const state = /** @type {StateHolder<MyState>} */(new StateHolder(undefined, {}));
We thought the JS and TS were exactly equivalent. However, in the JS version the final line const state
throws this type-checking error:
Conversion of type 'StateHolder<undefined>' to type 'StateHolder<MyState>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Type 'undefined' is not comparable to type 'MyState'.
We don't get type errors if we do this, but it's very verbose so would be nice to avoid it:
const state = new StateHolder(/** @type {MyState} */(/** @type {unknown} */(undefined)), {});
We may be able to get the correct typing by defining it in types.d.ts
but would rather do it in one file if we can.
Why do the JS and TS versions behave differently, and in JS how can we maintain the generic State
while passing in undefined
?