It's quite difficult to explain the goal, so feel free to ask if there's something unclear.
The end goal is that an object's shape will determine what functions are valid for it. I have a working implementation that does not constrain generics, which works okay. However, calling generics in a type safe way is bulky ( foo<typeof myConstraint.shape>({ /* ... */ }) or foo({ unusedValue: myConstraint.shape })). Is it possible to constrain it before the function is even passed to the user?
#Can a generic function be constrained further before usage?
55 messages · Page 1 of 1 (latest)
Preview:```ts
declare const dataTag: unique symbol
interface DataTag<T> {
[dataTag]?: T
}
// Users can extend it by any means with any type. Short example below.
interface Foo<T extends string> extends DataTag<T> {
dependentValue: T | null
}
function foo<T extends string>(params: Foo<T>)
...```
You can choose specific lines to embed by selecting them before copying the link.
Not exactly sure what your goal is, but here's one potential solution:
Preview:```ts
declare const dataTag: unique symbol
interface DataTag<T> {
[dataTag]?: T
}
interface Foo<T extends string> extends DataTag<T> {
dependentValue: T | null
}
function foo<T extends string>(params: Foo<T>) {}
type Constrain<TConstrainTo extends string> =
...```
You can choose specific lines to embed by selecting them before copying the link.
the constraint in this context is the major problem:
- I do not know the function in advance. The user passes
fooandFoo<T>, so they can have any shape with the only limit being that it extends<T>(params: DataTag<T>) => unknown
so there'd be other cases that are completely valid, like having it be called bar instead of foo
the runtime value is accessed from an object that the user created (Record<string, TheGenericFunction>), so maybe there's a way to do it using that record
Might be better to showcase how it's actually going to be used then.
sure, I'll write something basic with actual values
The actual use case is that there is currently a React implementation that uses context. However, it lacks a lot of type safety, and there are some basic types to help avoid wrongly assining things. However, derived properties are still a problem since they are generic.
Use case for context:
Preview:```ts
import React, {ReactNode} from "react"
// The user creates these React components for reusability
interface FooProps<T extends string> {
derivedValue: T
}
function Foo<T extends string>(props: FooProps<T>) {
// Accessing API through context
// @ts-expect-error
...```
You can choose specific lines to embed by selecting them before copying the link.
as listed in the use case, it's not impossible to work with it right now. I'm just curious if there's a way to let TypeScript handle this instead of having to add additional (unused) runtime values to get the typing right
Which parts can you change and which parts you cannot change?
type-wise, basically everything apart from Foo being a function.
- I can change it so users must have props extend my own (if typing can use it)
- I can force users to curry the component by passing it through another function (like
brandComponent(Foo)) - I can't change the record structure itself. Once assigned, that's the type I have to work with. I can only narrow it from there.
- I can't force the names of the record properties. That's up to the user.
- It's imperative that context is used, so even if an additional prop is added, it would just be unused at runtime
What's getApi, how come you can change it if Foo is implemented by user?
getApi exposes an API that helps with additional typings. The context provider for example accepts additional parameters that is dependent on the api and the props it receives.
the getApi is a created function based on the record that a user gave it. Internally, it creates everything API-related and assigns the components before passing it out again. Perhaps this is the better version to explain:
// const api1 = getApi('')
const { getApi } = createApi({ Foo });
const api1 = getApi('') // contains Foo
Foo is assigned to the API at the very end to allow users to have shorthand components that take over a lot of boilerplate that the API would otherwise expect from them
I feel like I'm missing some context, but I don't understand why you must forbid <api2.Foo derivedValue="" />
Foo is a component that works for any string derivedValue, so '' works too.
example: In Foo, if a button is clicked, it calls api.setValue(props.derivedValue).
That could be, for example, a Radio button. The context API stuff was made before this really showed up as a problem, and ideally you want to ensure that api.value and derivedValue must be compatible
right now, it would not warn you at all unless you added your own checks (like the unused api prop or typing the generic component explicitly)
Then why is Foo generic?
I mean, it's not required to be. If the derived value can be expressed without needing it, that's fine too
Preview:```ts
import React, {ReactNode} from "react"
interface FooProps<T extends string> {
derivedValue: T
}
const createApi =
<U extends string, P extends object>(options: {
Foo: (props: P & FooProps<U>) => React.JSX.Element
}) =>
<T extends U>(
...```
You can choose specific lines to embed by selecting them before copying the link.
This is what you want if I'm understanding correctly.
that looks really promising! I'll tinker with it later and see if there's still some hurdles to overcome. Thanks a lot!
The derived value seems to work! I assume one of the limitations of this is that the prop is not very flexible (must be called derivedValue, can't be an array of strings etc.), but it's progress!
the main problem (from what I can tell) is that in order to restrict the value, you need to know what property must be narrowed in advance. You can't narrow the whole generic or assert that if derivedValue: 'a' | 'b' => It must be FooProps<'a' | 'b'>
Should be able to get that to work too.
you mean narrowing an unknown generic interface by asserting one of its properties?
Can you show what your ideal API looks like?
The current progress is that the derived component works, but it's forced to use value: T and cannot be renamed or be a different shape.
That already covers like 90% of use cases, so it's not a big deal if custom shapes require the user to put in more work themselves.
That being said, here's kind of what an ideal API would look like (ignoring typing syntax).
The generics obv don't work when implemented this way, it's just to show what values are and aren't affected by the API value
// Normal components / Generic components that aren't derived. Can be added later since this is trivial
declare function Foo(props: FooProps): JSX.Element;
// Derived component. Props are dependent on the API's value
interface RadioButtonProps<ApiValue extends string> {
value: ApiValue;
label: string;
}
const RadioButton = createDerivedComponent(function RadioButton<T extends string>(props: RadioButtonProps<T>) {
// A button that sets the Api's value on click.
// The expected behaviour is that `props.value` must satisfy the Api value
return <></>
})
// A different example for derived component. Also dependent on the API's value
interface SelectProps<ApiValue extends string> {
options: ApiValue[];
default?: ApiValue;
label: string;
}
const Select = createDerivedComponent(function Select<T extends string>(props: SelectProps<T>) {
// A select menu that sets the Api's value on select.
// The allowed options are expected to satisfy the Api value.
return <></>
}
// Record can be freely named.
const { getApi } = createApi({ RadioButton, Menu: SelectMenu })
const api1 = getApi<string>()
<api1.RadioButton value="anything" label="" />
<api1.SelectMenu options={["anything", "works"]} label="" />
const api2 = getApi<'a' | 'b'>()
// value must be 'a' | 'b'
<api2.RadioButton value="a" label="" />
// options must be 'a' | 'b'
<api2.SelectMenu options={["a", "b"]} default="a" label="" />
For reference, the planned API currently looks like:
interface RadioButtonProps extends DerivedFieldProps<string> {
label: string
}
const RadioButton = createDerivedFieldComponent(function Test(
props: RadioButtonProps,
) {
const { label, value, api } = props
return (
<button type="button" onClick={() => api.setValue(value)}>
{label}
</button>
)
})
<api2.RadioButton value="a" label="Other" />
I'm not seeing how it relates to "must be called derivedValue"
How would your API look like if you were to allow user to specify a different prop name?
to narrow the props to the allowed values, it needs to know which property has to be narrowed
Yes and I'm saying it should be possible to allow user to specify the name instead of being hardcoded.
well, that derived shape differs not only from createApi to createApi, but it can differ between components as well
so that means
- the keys are dynamic (users can name it whatever,
RadioButton,Select,Foo) - the components can have any derived shape (
value,options,defaultValue, ideally the user can decide) - the API's value has to then influence how the properties are narrowed depending on what properties are derived and in what form
ApiValue, ApiValue[]
to think that going for a context-based system could make this so complicated ...
The simplest way is still to just do
interface FooGeneric<T> {
derivedValue: NoInfer<T>;
// unused by component, but ok for inference
api: { nested: { location: { here: T }
}
// ...
<api.FooGeneric api={api} value="a" />
which of course just makes the namespace pointless
Preview:```ts
import React, {ReactNode} from "react"
const createApi =
<P extends object>(options: {
Foo: (props: P) => React.JSX.Element
}) =>
<K extends keyof P, V extends string>(
key: K,
value: V
) => {
const Foo: (
props: P & Record<K, V>
) => React.JSX
...```
You can choose specific lines to embed by selecting them before copying the link.
Well I'm not quite sure what your API wants to look like, but the above example has a user configurable prop name via:
const api2 = getApi('bar', 'a' as 'a' | 'b')
Now the bar prop is configured to be 'a' | 'b' and will give a type error if you pass something else like bar="".
I'm not entirely sure myself. It's difficult to determine what the API should look like when I don't even know if there's a reasonable way to achieve it at all.
It really is just a TypeScript approach to not have to specify api here. That derivedValue or anything that the user specified has been narrowed beforehand
🤷
Maybe one design is to do:
const getApi = createApi({
RadioButton: {
component: RadioButton,
propName: 'value',
},
Menu: {
component: SelectMenu,
propName: 'selectedValue',
},
})
const api = getApi<'a' | 'b'>()
// api.RadioButton must have value = 'a' | 'b'
// api.Menu must have selectedValue = 'a' | 'b'
Should be pretty doable with the same idea as above.
yeah, maybe ... what about cases like options though, where it's modifying the derived value?
like api.Menu must have options = ('a' | 'b')[]
the more I think about it, the more I'm just tempted to recommend this instead. Chances are if you're using complex versions of the api value, then you also won't be using the complex component as often and this extra bit of code won't hurt
You'd probably need HKT for that case.
yeah, I'll check that out sometime. Fairly sure this is a good start with getting there. Thanks a lot for your support through this!
I think this discord server needs posts to be resolved, right? what do I type for that?
!resolve, I think.