[d3-axis and d3-format] activate strictNullChecks and strictFunctionTypes#24305
[d3-axis and d3-format] activate strictNullChecks and strictFunctionTypes#24305denisname wants to merge 10 commits intoDefinitelyTyped:masterfrom
Conversation
Function format can also take a string
|
@denisname Thank you for submitting this PR! 🔔 @tomwanzek @gustavderdrache @borisyankov - please review this PR in the next few days. Be sure to explicitly select If no reviewer appears after a week, a DefinitelyTyped maintainer will review the PR instead. |
|
@denisname The Travis CI build failed! Please review the logs for more information. Once you've pushed the fixes, the build will automatically re-run. Thanks! |
|
@denisname Thank you for submitting this PR. Given the extent of the contemplated changes, I will take some time to review them. In particular, since at first glance, you are proposing diverting from some of the conventions we have used in writing previous definitions. I will explain in my comments. I will take the time to digest and respond with motivations for some of the conventions we followed in the past. Should we reconsider some of them, I would prefer to have full alignment first and then update other definitions as appropriate for consistency. So that being said, it might be worthwhile to hold off on your proposed efforts with d3-array, until we have such alignment. This will save cycles for you and reviewers, should there be items where we prefer to hold on to current conventions. With 30+ definitions in the core D3 universe, conventions matter for a consistent user experience. Thanks in advance for your understanding. |
|
Ok. Note that I cut my changes into several small commits to make revision simpler. |
tomwanzek
left a comment
There was a problem hiding this comment.
Great work, a few asks for change and some (at times iterated) comments.
I will separately get back to the default generics and TS >=2.3 question in a follow-up comment.
types/d3-format/index.d.ts
Outdated
| * An optional array of ten strings to replace the numerals 0-9. | ||
| */ | ||
| numerals?: string[]; | ||
| numerals?: [string, string, string, string, string, string, string, string, string, string]; |
There was a problem hiding this comment.
I'm on the fence when it comes to this change. While it is strictly correct, It might be a little cumbersome, i.e. lengthy, when handling. Assuming the locale definitions are mostly generated "manually" in literal form, it will will add some semantic type safety.
However, if in the installed base we have users assigning string[]-type variables/constants to FormatLocaleDefinition.numerals, their existing code will break. This is true even if the numerals array was simply defined as const s = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; localeDef.numerals = s;, as s would be inferred as string[].
When I initially wrote these definitions, I abstained from [string, string, ..., string] because it seemed too clunky.
| */ | ||
| format(specifier: string): (n: number) => string; | ||
| format(specifier: string): (n: { valueOf(): number; } | string) => string; | ||
|
|
There was a problem hiding this comment.
I prefer to keep the existing definition as tight as it originally was, i.e. with a return type of (n: number) => string, we have historically taken the stance that TS definitions should be as semantically type-safe as possible, while being as flexible as necessary for real-world use cases. This is particularly true in the case of implicit type coercion sometimes done within D3 functions.
In the present case the official API documentation refers to a "number" being the input to the returned formatter-function. The test you added illustrates that you used a very artificial example, d3Format.format("$c")("☃"); which does not have real-world applicability, to test the relaxed interface. As a matter of fact, something like d3Format.format(".0%")("☃"); would return 'NaN%'.
We have kept these formatter functions tight to a number-argument without anyone having raised an issue in the past as to lack of flexibility in real life applications. If a consumer of these particular definitions wants to coerce an input to number, they should do so explicitly, knowing that it makes sense in their particular use case, i.e. d3Format.format(".0%")(+x); where x is coercible.
More generally, as I pointed out in my issue comment to your question about numeric corecibles, the omission of number makes the TS definition extremely hard to read for human users even when numeric coercibles are permissible inputs. See item (3) in the comment.
The definitions must serve two masters: The human being that is the developer and the TS compiler. Usability is important.
There was a problem hiding this comment.
Update: Upon further review of the integration with d3-axis, let us go with the following return type for all format(...) and formatPrefix(...) functions:
(n: number | { valueOf(): number }) => stringWe will need the support for coercible numeric values, as we support it on axis-/scale-related features. But please keep the number in the union for human readability to avoid unnecessary cognitive load for the average human user.
There was a problem hiding this comment.
The matter of specifier type "c" is interesting in that the official API doc does not reflect the implemented code since this change to the d3-format code. The official API doc still refers to the old behaviour:
c - converts the integer to the corresponding unicode character before printing.
and the reference to the argument of the returned formatter function being a "number". So good on you for finding a rather undocumented behaviour in the source.
Seems to me on yet further consideration, that (a) the API doc should be updated to reflect the current behaviour (PR to d3-format) AND (b) we need a more complex explanation of the permissible inputs to the formatter functions as well, i.e. when an actual text string is permissible in combination with format-specifiers of type "c".
So, I stand corrected on the extension to also include string explicitly. No-one seems to have used it so far in actual applications with these TS definitions, otherwise we would have likely heard about it.
| formatPrefix(specifier: string, value: number): (n: number) => string; | ||
| formatPrefix(specifier: string, value: number): (n: { valueOf(): number; }) => string; | ||
| } | ||
|
|
There was a problem hiding this comment.
Same comment as for format(...), please keep the original return type signature.
There was a problem hiding this comment.
Update: Upon further review of the integration with d3-axis, let us go with the following return type for all format(...) and formatPrefix(...) functions:
(n: number | { valueOf(): number }) => string
types/d3-format/index.d.ts
Outdated
| * @throws Error on invalid format specifier. | ||
| */ | ||
| export function format(specifier: string): (n: number) => string; | ||
| export function format(specifier: string): (n: { valueOf(): number; } | string) => string; |
There was a problem hiding this comment.
Please revert to original signature. See comments above regarding format() and formatPrefix().
There was a problem hiding this comment.
Update: Upon further review of the integration with d3-axis, let us go with the following return type for all format(...) and formatPrefix(...) functions:
(n: number | { valueOf(): number }) => string
types/d3-format/index.d.ts
Outdated
| * @throws Error on invalid format specifier. | ||
| */ | ||
| export function formatPrefix(specifier: string, value: number): (n: number) => string; | ||
| export function formatPrefix(specifier: string, value: number): (n: { valueOf(): number; }) => string; |
There was a problem hiding this comment.
Please revert to original signature. See comments above regarding format() and formatPrefix().
There was a problem hiding this comment.
Update: Upon further review of the integration with d3-axis, let us go with the following return type for all format(...) and formatPrefix(...) functions:
(n: number | { valueOf(): number }) => string
types/d3-axis/d3-axis-tests.ts
Outdated
| leftAxis = leftAxis.scale(scalePow()); | ||
| const powerScale: ScalePower<number, number> = leftAxis.scale<ScalePower<number, number>>(); | ||
| // powerScale = leftAxis.scale(); // fails, without casting as AxisScale is purposely generic | ||
| const powerScale: ScalePower<number, number> = leftAxis.scale() as ScalePower<number, number>; |
There was a problem hiding this comment.
Use declarative castingleftAxis.scale<ScalePower<number, number>>()
types/d3-axis/d3-axis-tests.ts
Outdated
|
|
||
| // -------------------------------------------------------------------------- | ||
| // Strict Function Types | ||
| // -------------------------------------------------------------------------- |
There was a problem hiding this comment.
Laudable idea to think of explicitly testing strictFunctionTypes. However, it will not work, as you found, since dts-lint is running from the minimally permitted TS version up to the latest version. So for anything below TS 2.6 the tests will not fail as expected, as strictFunctionTypes were not enforced.
So I would simply remove this section.
| containerElement = svg; | ||
| containerElement = g; | ||
| // containerElement = canvas; // fails, incompatible type | ||
| // $ExpectError |
There was a problem hiding this comment.
Thanks for starting to use this dts-lint feature. We should start using this across the board on all the "fail" tests in D3. We had to historically comment them out (and manually check during definition updates). So let's roll with $ExpectError
| gTransition.call(topAxis); | ||
|
|
||
| const svgSelection: Selection<SVGSVGElement, any, any, any> = select<SVGSVGElement, any>('g'); | ||
| const svgSelection: Selection<SVGSVGElement, any, any, any> = select<SVGSVGElement, any>('svg'); |
There was a problem hiding this comment.
Good catch 😄 Now that's attention to detail!
| * Interface defining an axis generator. The generic <Domain> is the type of the axis domain | ||
| * Interface defining an axis generator. The generic <Domain> is the type of the axis domain. | ||
| */ | ||
| export interface Axis<Domain> { |
There was a problem hiding this comment.
I will review the constraints on Domain in the definition for Axis and AxisScale to define a suitable default for the generic parameters (TS 2.3 feature that we should use here as well). We might be able to do better than any for the most general permissible case.
We could have export interface Axis<Domain extends DomainConstraint = DomainDefault> {...} and export interface AxisScale<Domain extends DomainConstraint = DomainDefault> {...}. Then in day-to-day use one could just do let x: Axis; for brevity, if the defaults are good enough.
|
As for a minimal constraint on the generic export type AxisDomain = number | string | Date | { valueOf(): number};and then impose: export interface AxisScale<Domain extends AxisDomain> {...}
...
export interface Axis<Domain extends AxisDomain> { ... }
...
export function axisTop<Domain extends AxisDomain>(scale: AxisScale<Domain>): Axis<Domain>;
export function axisRight<Domain extends AxisDomain>(scale: AxisScale<Domain>): Axis<Domain>;
export function axisBottom<Domain extends AxisDomain>(scale: AxisScale<Domain>): Axis<Domain>;
export function axisLeft<Domain extends AxisDomain>(scale: AxisScale<Domain>): Axis<Domain>;Defaulting the generic for |
Change back numerals type Change format and formatPrefix
|
@denisname Penny for your thoughts...:smile: |
Edit: CI build failed as expected, see "scale generic". |
|
The I also tried to solve the problem of ticks et ticksFormat typing and to remove: For this, I separated export interface AxisScale<Domain> {
(x: Domain): number | undefined;
domain(): Domain[];
range(): number[];
copy(): this;
bandwidth?(): number;
}
export interface TickedAxisScale<Domain, Tick> extends AxisScale<Domain> {
ticks(count?: Tick): Domain[];
tickFormat(count?: Tick, specifier?: string): ((d: Domain) => string);
}I then removed export interface TickedAxis<Domain, Tick> extends Axis<Domain> {
// ...
ticks(count: Tick, specifier?: string): this;
tickArguments(): [Tick] | [Tick, string];
tickArguments(args: [Tick] | [Tick, string]): this;
}
export function axisTop<Domain, Tick>(scale: TickedAxisScale<Domain, Tick>): TickedAxis<Domain, Tick>;
export function axisRight<Domain, Tick>(scale: TickedAxisScale<Domain, Tick>): TickedAxis<Domain, Tick>;
export function axisBottom<Domain, Tick>(scale: TickedAxisScale<Domain, Tick>): TickedAxis<Domain, Tick>;
export function axisLeft<Domain, Tick>(scale: TickedAxisScale<Domain, Tick>): TickedAxis<Domain, Tick>;Now type inference detect which types are allowed for d3Axis.axisTop(scaleLinear()).ticks(1, ',f'); // Ok
d3Axis.axisTop(scaleLinear()).ticks(timeMinute.every(5)!, ',f'); // failsProvided that, in In the gist I also added Hope my explanations are clear. Waiting for your comments. :) |
|
@denisname Thanks, I will respond at my earliest. Unfortunately, I am little otherwise-committed right now 😄 |
|
Thanks for your contribution. This PR has failing CI tests and can not be merged in at the time being. For housekeeping purposes we are closing stale PRs. If you'd still like to merge this code in, please open a new PR that has been merged and rebased with the |
|
@denisname My apologies for not having been faster than, it's been a little hectic. I would love to resurrect your work and get it going. There are a few options (bar reopening this PR and re-basing it): Then we take it from there... As for the the specific comment on scale<A extends AxisScale<Domain> = AxisScale<Domain>>(): A;for now. It will allow These kind of changes have been a bit of a thorn in our side, because we cannot semantically version the definitions in their own right. I will create an RFC item to discuss something that has been on my mind for a while now. I will @-mention you alongside @gustavderdrache and @Ledragon as key stakeholders. |
|
PS: Ignore the comment suggesting to add the default to the generic in As for the lines that create problems for If we are still running into issues where the error assertion does not work in some TS version, but works in others, maybe we should just remove that particular definitions test. |
In this PR, I activated strictFunctionTypes in d3-axis. To get it work I also needed to update d3-format.
In d3-format:
{ valueOf(): number; }informatandformatPrefix.formatcan take a string, see format-type-c-test.js.@throwsin JsDoc.In d3-axis:
"unified-signatures": falsefromtslint.json.$ExpectErrorfor failing tests.Selection<SVGSVGElement | SVGGElement, any, any, any>bySelection<SVGSVGElement, any, any, any> | Selection<SVGGElement, any, any, any>.I add a test for
strictFunctionTypes. Not sure if it is the best way to do it. Also CI build fail for them because some$ExpectErrorlines are not detected as failing. I'm wondering ifdtslinttest the code with TS 2.7 or not.Finally, I don't think any new features from TS 2.3 may improve those definitions. So I kept the minimum version to 2.0.
Related: #23611.
Please fill in this template.
npm test.)npm run lint package-name(ortscif notslint.jsonis present).Select one of these and delete the others:
If changing an existing definition:
tslint.jsoncontaining{ "extends": "dtslint/dt.json" }.