uPlot icon indicating copy to clipboard operation
uPlot copied to clipboard

[ts] conflicting font types

Open vthriller opened this issue 3 years ago • 11 comments

uPlot.Axis currently defines:

        font?: CanvasRenderingContext2D['font'];
        labelFont?: CanvasRenderingContext2D['font'];

While true for values passed into constructor, various callbacks actually need to deal with values processed by pxRatioFont(), which, it seems, actually has (non-null? not sure about that) return type [string, number, number].

vthriller avatar Oct 07 '21 07:10 vthriller

this is a general typings issue unfortuntely. a lot of the passed Options are internally converted to getters (or in this case parsed tokens) and re-exposed as such. when passed during init they may be optional or multiple types but after init, they are non optional and exposed uniformly for read-back. i'm not sure how to express these semantics without duplicating all types with slight tweaks and bloating the typings file + maintenance nightmare. if you have a good solution i'm eager to try it!

leeoniya avatar Oct 07 '21 12:10 leeoniya

I'm not sure exactly what the typing issue is, but would something like the below work?:

interface Options {
	a?: string;
	b?: [string, number];
	c?: number;
}
let opt: Options = {}; // no TS errors

type Options2 = {
	[P in keyof Options]-?: // the "-?" means "remove the optional flag for these properties"
		Options[P] extends string|undefined						? [string, boolean] :
		Options[P] extends [string, number]|undefined			? [string, number, boolean] :
		Options[P] extends number|undefined						? [number, boolean] :
		never;
}
let opt2: Options2 = { // no TS errors
	a: ["", true],
	b: ["", 1, true],
	c: [1, true],
};

It's just an example showing how you can use TypeScript conditional-types/type-extensions (not sure the correct terminology) to loop through the properties in one type, and replace them with another set of types.

EDIT: Ah, found some documentation on it: https://www.typescriptlang.org/docs/handbook/2/mapped-types.html

Venryx avatar Oct 07 '21 14:10 Venryx

interesting. not sure that route is ideal though.

effectively i want:

interface Options {
  a?: string;
  b?: [string, number] | (u) => number;
  c?: number;
}

// 1. make a,b,c non-optional
// 2. re-use the callback variant of Options.b (something like <Pick> for type disjunctions)
interface ComputedOptions extends Options {
  b: (u) => number
}

i guess 2. can be handled by defining a new type for the callback variant of b, but this would need to be done in a ton of places, which i'd like to avoid.

generally, these re-exposed options are not terribly useful externally because they sometimes require extra params that are only available from internal calcs. i'm not sure it's worth investing this effort rather than simply passing down the original options object alongside the uplot instance if you really want to read back the original settings as they were provided.

leeoniya avatar Oct 07 '21 15:10 leeoniya

i guess 2. can be handled by defining a new type for the callback variant of b, but this would need to be done in a ton of places, which i'd like to avoid.

Would something like this work?:

interface Options {
  a?: string;
  b?: number;
  c?: [string, number] | (u) => number;
}
type ComputedOptions = {
	[P in keyof Options]-?: // the "-?" means "remove the optional flag for these properties"
		// for any field where the type definition is undefined|<function type>|[X, Y?, Z?], change it to only accept the declared function-type
		Options[P] extends undefined|Function|[infer T1]								? Extract<Options[P], Function> :
		Options[P] extends undefined|Function|[infer T1, infer T2]						? Extract<Options[P], Function> :
		Options[P] extends undefined|Function|[infer T1, infer T2, infer T3]			? Extract<Options[P], Function> :
		Options[P];
}

In the above, ComputedOptions thus becomes:

interface ComputedOptions {
  a: string;
  b: number;
  c: (u) => number;
}

Giving this usage:

// valid
const o1: ComputedOptions = {a: "", b: 1, c: a=>3};
// invalid
const o2: ComputedOptions = {a: "", b: 1, c: 1};
const o3: ComputedOptions = {a: "", b: 1, c: "1px"};

Venryx avatar Oct 07 '21 15:10 Venryx

how gnarly would this look with nested options?

ComputedOptions
  series: ComputedSeriesOptions
  axes: ComputedAxesOptions
    grid: ComputedGridOptions

seems like we'd get some really complex or multi-layer generic typings that i wont be able to fully wrap my head around or maintain effectively. at that point just copy/paste/modify starts looking pretty good, but 🤷

leeoniya avatar Oct 07 '21 17:10 leeoniya

You can use a generic type-replacer system:

type NarrowMultiTypeFieldsToJustFuncType<T> = {
	// the "-?" means "remove the optional flag for these properties"
	[P in keyof T]-?: ExtractFuncTypeFromMultiType<T[P]>;
}
type ExtractFuncTypeFromMultiType<T> =
	// where type is undefined|<function type>|[X, Y?, Z?], extract only the declared function-type
	T extends undefined|Function|[infer T1]									? Extract<T, Function> :
	T extends undefined|Function|[infer T1, infer T2]						? Extract<T, Function> :
	T extends undefined|Function|[infer T1, infer T2, infer T3]				? Extract<T, Function> :
	// for other types, leave unmodified
	T;

Then do this:

type ComputedOptions = {
	series: NarrowMultiTypeFieldsToJustFuncType<ComputedSeriesOptions>;
	axes: NarrowMultiTypeFieldsToJustFuncType<ComputedAxesOptions>;
	grid: NarrowMultiTypeFieldsToJustFuncType<ComputedGridOptions>;
}

Venryx avatar Oct 07 '21 18:10 Venryx

i should have been more clear that ComputedGridOptions are nested inside ComputedAxesOptions (axis.grid).

leeoniya avatar Oct 07 '21 18:10 leeoniya

Ah. Well, that shouldn't complicate things too much, as the generic type-helper can be used on multiple levels.

I guess the main question is whether it's consistently the case that, when a field has multiple types, you always want the callback-type to be used as the field's ComputedOptions type. (if that's not the case, then hard-coding may still be better)

Venryx avatar Oct 07 '21 20:10 Venryx

I guess the main question is whether it's consistently the case that, when a field has multiple types, you always want the callback-type to be used

yes, i think this is pretty much always the case.

leeoniya avatar Oct 07 '21 21:10 leeoniya

before committing to this route, i'd like to see an initial scaffold PR for what this might look like; don't bother doing the entire typings file, but a decent amount of it that covers different cases, including multi-level nested computed types. if it looks reasonable enough to maintain, then we can go ahead.

leeoniya avatar Oct 08 '21 22:10 leeoniya

I unfortunately do not have the time/motivation for this right now. I can provide technical support (ie. answers to "how do I make the type system do this?" questions) if anyone else wants to take on the job, though. (the type-transformers above should be enough to get started)

Venryx avatar Oct 09 '21 03:10 Venryx