#Satisfies [Maybe Variance] Trouble

220 messages · Page 1 of 1 (latest)

high pelican
#

Hey there! I'm having an issue with generics, a callback, etc when it comes to satisfies, and I think it might be related to variance (ie. #1241494319982968924 message)

However, I'm unsure if that's the case, and unsure how to fix it either way. If someone could take a look, I would greatly appreciate it!

TSP: http://zsl.io/QdXu4v

stuck cape
#

The set method requires a callback that can accept TState. The instance can then call that callback knowing that it can handle TState.

If you create StateBagConcrete with a narrow TState, then it accepts callbacks that can only handle that narrow TState.

If you try to use StateBagConcrete<Narrow> as StateBagConcrete<Wide>, then StateBagConcrete<Wide> could try to call the callback with a type that is too wide for the narrow callback.

#

When receiving data, you can accept a narrow type. When sending data, the destination type can be wider. With a callback, the callback is the destination so it's type can be wider than the state constraint, not narrower.

#

The constraints are reversed because the data flow is going in the other direction

#

--
Technically function foo(array: (string | number)[]) {} should not accept an array of type string[] either, because we can read from + write to arrays, so the array accepted should be compatible both ways, so exactly (string | number)[], but TS kind of ignores this and lets you do unsafe stuff.

earnest frigateBOT
#
function foo(cb: (v: string | number) => void) {}

const f1 = (v: string) => {}
foo(f1) // Bad
//  ^^
// Argument of type '(v: string) => void' is not assignable to parameter of type '(v: string | number) => void'.
//   Types of parameters 'v' and 'v' are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

const f2 = (v: string | number | boolean) => {}
foo(f2) // OK
high pelican
#

@stuck cape Ok, it took a bit, but I made a stripped down version of my codebase to show you the broader picture of what I'm trying to accomplish: http://zsl.io/7lJWYz

If you are willing to take a look at it, I would be hugely grateful. I've been struggling for a couple days on it now.

Look at examples.tsx to see the top-to-bottom flow of how I intend to use it

Some key things I need:

  • Feature instances need to optionally be able to have additional properties and methods (so I can write custom methods to manage state, etc)
  • Accessing specific features by key in the FeaturesMap (ie. featuresInstance.getFeature("featureKey") needs to return the specific type-safe instance of a Feature, including its custom methods

I really appreciate any help you (or anyone) gives. Thank you!

stuck cape
#

Can you explain what you're trying to do?

#

You can't really have a type with custom properties and methods, because that isn't statically typed

#

You can have something like Record<string, unknown> that can have any property, but you still have to inspect it to see which properties it has when you use it

#

So you want to create a new type instead

#

An easy way to make a feature with extra props/methods is to extend the Feature class

earnest frigateBOT
#
type Expand<T> = {
  [K in keyof T]: T[K];
} & {};

class Feature {
    a = 1
}
const v = new (class Feature2 extends Feature {
    b = 2
})

   v
// ^? - const v: Feature2
type V = Expand<typeof v>
//   ^? - type V = {
//       b: number;
//       a: number;
//   }
stuck cape
#

And your FeaturesMap type can be something like

export type FeaturesMap = Record<
  string,
  Feature<Context, never> | ExtendedFeature<Context, never>
>;
#

never is the opposite of unknown, so when you have reverse constraints it acts like unknown

high pelican
#

@stuck cape I did forget to mention that I already have d the feature class in order to get the extra properties. I had that type inference working at some point, and I’m not sure why it was, but there were obviously other types within the internals of those classes and functions, that weren’t of what I was trying to do.

#

My goal was to make sure the features map would complain if you passed it that didn’t at least adhere to the minimal interface of a feature. But I wanted it to be tolerant of extra properties on those objects.

#

And I honestly have never looked up the never keyword and don’t really understand it yet. I’m gonna look into it then a minute here.

So what exactly is never doing in your example, when you say it does the opposite of unknown. What if I wanted my extended state to also have a minimal interface with some known properties in it?

stuck cape
#

Unknown contains all types, like a union of everything

#

Never is contained within all types

#

You can pass never to boolean, and Boolean to unknown

#

If you use never as a type for a callback argument, you're saying you're passing a type that can be assigned to anything, so any argument type is permitted

earnest frigateBOT
#
const f: (a: never) => unknown = (a: boolean) => 5
//    ^? - const f: (a: never) => unknown
#
const f: (a: unknown) => unknown = (a: boolean) => 5
//    ^
// Type '(a: boolean) => number' is not assignable to type '(a: unknown) => unknown'.
//   Types of parameters 'a' and 'a' are incompatible.
//     Type 'unknown' is not assignable to type 'boolean'.
//    ^? - const f: (a: unknown) => unknown
high pelican
stuck cape
#

Why don't you extend the Feature class instead?

#

Then you get actual typed objects, instead of vague records

#

An instance of a class that extends Feature is a kind of Feature, so you don't needed ExtendedFeature

high pelican
#

Like if this was my goal:

type BaseState = {
  knownProp: number;
}

type ExtendedState = {
  requiredUserProp: string;
}

type CompleteState = BaseState & ExtendedState;

Feature<Context, Extended> state as the minimum interface. 
#

I am extending the feature class to get the extra properties when creating concrete implementations, but I wanted to create interface types that represent what the library expects you to pass it. I don’t care if myself or another developer uses classes or whatever other method to create the instances, I just care what the minimal public properties are so I can interact with them. Just basic interface stuff.

stuck cape
#

Are the custom properties/methods restricted to some particular type?

high pelican
#

Nope. It just needs to be tolerant of them, but if I access that map by a specific key as the developer who created that instance, I need to be able to access that custom method and have it know what it is. Internally in the features container class, it doesn’t needto know about those. It’s just that without doing something about it, the types were intolerant of you adding extra properties to it.

stuck cape
#

TS doesn't care about extra properties on objects, you don't need to do anything to allow them

high pelican
#

Sorry, I’m on my phone at the moment and can’t type out code examples very well, but an example would be that the features class needs to be able to call methods like isEnabled(), and the author developer could create special methods to modify his own state more easily.

high pelican
earnest frigateBOT
#
class Feature {
    a = 1
}
const f = new Feature



class MyFeature extends Feature {
    b = 1
}
const mf = new MyFeature

const o: Record<string, Feature> = {
    f, 
    mf
}

   o
// ^? - const o: Record<string, Feature>
high pelican
#

But going back to the callback issue, if you look at my code, you can probably see what I was trying to do with the extended state, but then the compiler was yelling

stuck cape
#

Make a smaller demo on TS Playground

#

I can't edit the demo you provided

earnest frigateBOT
#
sandiford#0

Preview:```ts
class Feature {
a = 1
}
const feature = new Feature

class MyFeature extends Feature {
b = 1
}
const myFeature = new MyFeature

const featureMap = {
feature,
myFeature
} as const satisfies Record<string, Feature>

featureMap
// ^?

featureMap.myFeature
...```

#
sandiford#0

Preview:```ts
class Feature<CbArg> {
a = 1
cb
constructor(cb: (arg: CbArg) => void) {
this.cb = cb
}
}
const feature = new Feature<string>((arg: string) => {})

class MyFeature<CbArg> extends Feature<CbArg> {
b = 1
}
const myFeature = new MyFeature<number>((arg: number) => {})
...```

stuck cape
#

Here is a basic demo with a Feature, an extended Feature, a callback, and a map

high pelican
stuck cape
#

And because the map is created literally with as const, it retains all type info

#

There's a lot of stuff in your demo that seems off, it's not a single problem, rather probably a general misunderstanding

#

so I'll have to go through it with you bit by bit, unless you can look at my demo and understand that.

high pelican
#

I'm sure there are plenty of things that are off, I use TS all the time, but I'm not extremely well versed with it.

stuck cape
#
export type BaseState = {
  isEnabled?: boolean;
};
#

Ok BaseState is fine

high pelican
#

I actually forgot about the as const thing

stuck cape
#
export type ExtendedState = Record<string, any>;

this is probably pointless, it tells is nothing

#

Well, I suppose its a contain to using only string keys, so its OK as a constraint

high pelican
#

Well yes, but my example was that what if I want an expected property in there?

#

Yea, I guess that's valid. I want to merge it with another state object of Record<string, any>

stuck cape
#

The simple thing is

T extends BaseState
#

This lets you add additional properties

#

You only need Record<string, any> if you want to make sure that extra properties have use string keys, not number/symbol keys

#

But do you actually want that?

high pelican
#

It's not 100% necessary I guess, but I want to be able to do stuff like const state = { ...baseState, ...extendedState }

stuck cape
#

sure that's not related

#

So let me get rid of the records for now

high pelican
#

So I need to know it's a mergable object

stuck cape
#

every object is mergable

#

You capture the data from the generic, a Record doesn't actually give us any information

#

it just says what the object can have, which isn't very helpful

#

Records are for where you want to accept arbitrary entries without having specific type information, not useful for most things.

high pelican
#

I haven't put it in my example, but I like to do stuff like this with my callbacks:

{
  set:(cb: TState | (prevState: TState) => TState) => {
    if (_.isFunction(cb) {
      this.state = cb(this.state);
    }

    this.state = { ...this.state, ...cb };
  };
}
high pelican
stuck cape
#

A record doesn't help with that

#

OK so I have this

export type BaseState = {
  isEnabled?: boolean;
};

export type BaseContext = object

export type Feature<
  TContext extends BaseContext = BaseContext,
  TState extends BaseState = BaseState
> = {
  isEnabled: () => boolean;
  getState: () => StateBag<TState>;
};
#

Because TState is generic, TState will contain all the information of the object type

high pelican
#

Yea, I'm doing that everywhere as you noticed lol

stuck cape
#

We don't need ExtendedFeature, because we can just extend Feature ourselves

high pelican
#

Ok, that's fine, that was my misunderstanding

#

I was getting a "customMethod ... something about it missing from the other feature or something internal" error

stuck cape
#

For FeatureMap we would normally use our upper constraints for Feature, which would be Feature<BaseContext, BaseState>, but because BaseState is outgoing data not incoming data, we need the reverse constraint which is never

#
export type BaseState = {
  isEnabled?: boolean;
};

export type BaseContext = object

export type Feature<
  TContext extends BaseContext = BaseContext,
  TState extends BaseState = BaseState
> = {
  isEnabled: () => boolean;
  getState: () => StateBag<TState>;
};

export type FeaturesMap = Record<
  string,
  Feature<BaseContext, never>
>;
#

What's the StateBag stuff?

high pelican
#

Ok I see, the Feature type itself handles whether the user's TState adheres to BaseState, but we're telling FeatureMap that we can't know what they all are specifically?

stuck cape
#

We're effectively telling FeatureMap to accept any Feature

high pelican
#

StateBag is a simple object that manages internal state and persists it to storage, rehydrates it, fires events, etc

stuck cape
#

And TS only cares about known properties, so if we add properties TS doesn't care

high pelican
#

Think of Zustand w/ persist middleware, but with a public API that makes sense for what I'm trying to do and no deps

stuck cape
#

So it seems odd to me that the State in StateBag doesn't include BaseState

#
type ExampleState = {
  test: number;
};

const stateBag = new StateBagConcrete<State<ExampleState>>()

Should a stateBag bag include isEnabled?

high pelican
#

Because BaseState is specific to the features lib, StateBag is generic and will take anything

stuck cape
#

ah

#
export type StateBag<TState extends object = object> = {
  set: (cb: (state: TState) => TState) => void;
};

class StateBagConcrete<TState extends object = object> implements StateBag<TState> {
  public set = (cb: (state: TState) => TState) => {}
}
high pelican
#

So I combine it all before I store it, but I don't want the user passing in BaseState. They can specific "additional" (Extended) state to store alongside it

stuck cape
#

So we removed the Record here too

high pelican
#

Ok

stuck cape
#

This could be an abstract class instead of a type

#
abstract class AbstractStateBag<TState extends object = object> {
  set: (cb: (state: TState) => TState) => void;
}
#

But, same difference

high pelican
#

It could be, but I like to define things as minimal types, then implement them, rather than doing stuff like this:

class Concrete {}

type ConcreteInstance = typeof Concrete;

import type { ConcreteInstance } from "./...";

type SomethingElse<TConcrete extends ConcreteInstance> = { ... }
#

Same with Feature, etc

stuck cape
#

🤷🏻‍♂️

#

Whatevs

earnest frigateBOT
#
export type BaseState = {
  isEnabled?: boolean;
};

export type BaseContext = object

export type Feature<
  TContext extends BaseContext = BaseContext,
  TState extends BaseState = BaseState
> = {
  isEnabled: () => boolean;
  getState: () => StateBag<TState>;
};


export type FeaturesMap = Record<
  string,
  Feature<BaseContext, never>
>;


export type StateBag<TState extends object = object> = {
  set: (cb: (state: TState) => TState) => void;
};

class StateBagConcrete<TState extends object = object> implements StateBag<TState> {
  public set = (cb: (state: TState) => TState) => {}
}

//

type ExampleState = {
  test: number;
};

const stateBag = new StateBagConcrete<ExampleState>()

const feature = {
  isEnabled: () => false,
  getState: () => stateBag,
//                ^^^^^^^^
// Type 'StateBagConcrete<ExampleState>' is not assignable to type 'StateBag<BaseState>'.
//   Types of property 'set' are incompatible.
//     Type '(cb: (state: ExampleState) => ExampleState) => void' is not assignable to type '(cb: (state: BaseState) => BaseState) => void'.
//       Types of parameters 'cb' and 'cb' are incompatible.
//         Types of parameters 'state' and 'state' are incompatible.
//           Type 'ExampleState' has no properties in common with type 'BaseState'.
} satisfies Feature;
stuck cape
#

So we get to this stage, and this is probably the same error as before. stateBag doesn't include isEnabled

#

So here we should be including isEnabled in our ExampleState I guess?

high pelican
#

An example of why - If I need a simple storage interface:

type Storage<TData extends object> = {
  get: () => Promise<TData | undefined>;
  set: (data: TData) => any; 
}

That's all I care about. I don't want to force myself or other devs to extend a class.

stuck cape
#

sure

high pelican
stuck cape
#

I think I haven't understood yet, so I will look some more

high pelican
#

Your example above is a little off, one sec

stuck cape
#
abstract class AbstractStateBag<TState extends object = object> {
  abstract set: (cb: (state: TState) => TState) => void;
}

type ExampleState = { a: string }

const stateBag = {
    set: (cb: (state: ExampleState) => ExampleState) => {}
} satisfies AbstractStateBag<ExampleState>
#

So you can use satisfies with an abstract class without having to extend it

high pelican
#
// Internal state
export type BaseState = {
  isEnabled?: boolean;
};

// User provided state
export type ExtendedState = object;
// Or I could enforce the user has specific properties in here as well
export type State<TExtendedState extends ExtendedState> = BaseState & TExtendedState;

export type BaseContext = object

export type Feature<
  TContext extends BaseContext = BaseContext,
  TExtendedState extends ExtendedState = ExtendedState
> = {
  isEnabled: () => boolean;
  getState: () => StateBag<State<TState>>;
};


export type FeaturesMap = Record<
  string,
  Feature<BaseContext, never>
>;


export type StateBag<TState extends object = object> = {
  set: (cb: (state: TState) => TState) => void;
};

class StateBagConcrete<TState extends object = object> implements StateBag<TState> {
  public set = (cb: (state: TState) => TState) => {}
}

//

type ExampleState = {
  test: number;
};

const stateBag = new StateBagConcrete<ExampleState>()

const feature = {
  isEnabled: () => false,
  getState: () => stateBag,
//                ^^^^^^^^
// Type 'StateBagConcrete<ExampleState>' is not assignable to type 'StateBag<BaseState>'.
//   Types of property 'set' are incompatible.
//     Type '(cb: (state: ExampleState) => ExampleState) => void' is not assignable to type '(cb: (state: BaseState) => BaseState) => void'.
//       Types of parameters 'cb' and 'cb' are incompatible.
//         Types of parameters 'state' and 'state' are incompatible.
//           Type 'ExampleState' has no properties in common with type 'BaseState'.
} satisfies Feature;
high pelican
stuck cape
#

You can import just the type

#

TS creates a type silently when you create a class

high pelican
#

Oh, I didn't know that

#

I was trying to avoid circular imports

stuck cape
#

Yeah that's why you can do const foo: Feature = new Feature

#

it has to create a Feature type for that to work

high pelican
#

That's interesting. Idk what made me think you can't do that, but it makes sense.

stuck cape
#

OK so here

export type Feature<
  TContext extends BaseContext = BaseContext,
  TExtendedState extends ExtendedState = ExtendedState
> = {
  isEnabled: () => boolean;
  getState: () => StateBag<State<TExtendedState>>;
};

We are actually not using StateBag as an arg, it is just a return value? So it's has normal variance?

high pelican
#

Yea, that is just a getting for the state instance, which has the problematic cb method on it

earnest frigateBOT
#
sandiford#0

Preview:```ts
export type BaseContext = object

// Internal state
export type BaseState = {
isEnabled?: boolean;
};

// User provided state
export type ExtendedState = object;
// Or I could enforce the user has specific properties in here as well
export type State<TExtendedState extends ExtendedState> = BaseState & TExtendedState;
...```

stuck cape
#

Right so the callback is in the statebag

high pelican
#

Yup

stuck cape
#

Might be an any job

#

Because our ExampleState appears in both a receiving position and a sending position

high pelican
#

Your FeaturesMap type is wrong in there

stuck cape
#

the only way we can make the types match up in both positions is to specify an exact type

high pelican
high pelican
stuck cape
#

It's not weird. Basically you will either need an exact type, or you'll need to use any to be flexible

high pelican
#

Not really, I started getting like 10% lost on that one

stuck cape
#

So this is pretty simple

#

If we have an array: (string | number)[]

#

Give me a sec to write it

#
const array: (string | number)[] = [1, 2, 'foo', 'bar']

// it is safe to read from this array into a wider var
const v1: string | number | boolean = array[0] // array[0] is definitely within string | number | boolean

// but we can't read to a narrower var
const v2: string = array[0] // array[0] is a number, this is invalid
#

So normally in TS we are reading from a source, to a typed location

#

If we read from the array to a wider type string | number | boolean it is safe

#

But if we want to write to the array, the data direction is reverse, and so are the constraints

earnest frigateBOT
#
const array: (string | number)[] = [1, 2, 'foo', 'bar']

// it is safe to read from this array into a wider var
const v1: string | number | boolean = array[0] // array[0] is definitely within string | number | boolean

// but we can't read to a narrower var
const v2: string = array[0] // array[0] is a number, this is invalid
//    ^^
// Type 'string | number' is not assignable to type 'string'.
//   Type 'number' is not assignable to type 'string'.

const d1 = 'foo'
array.push(d1) // it's totally fine to push a string to the (string | number)[] array

const d2: string | number | boolean = true
array.push(d2) // but we can't send a wider type to the array
//         ^^
// Argument of type 'boolean' is not assignable to parameter of type 'string | number'.
stuck cape
#

So what you should notice here is that when reading data, we can read to a wider type

#

and when writing data we can write to a wider type

#

but we can never read to a narrow type, or write to a narrower type

#

v1: string | number | boolean is fine when reading, because data is coming to it.
d2: string | number | boolean is not fine when writing, because we are sending a wider type to a narrower type

high pelican
#

That all makes sense, I think I wasn't able to understand exactly what you were referring to when you said "With a callback, the callback is the destination so it's type can be wider than the state constraint, not narrower. The constraints are reversed because the data flow is going in the other direction"

#

Not so much that I don't understand the concept, I just couldn't figure out what to do about it.

#

But I think your FeaturesMap in your TSP should be:

export type FeaturesMap = Record<
  string,
  Feature<BaseContext, never>
>;

If I recall your notes correctly

stuck cape
#
function foo(array: (string | number)[]) {
    const v = array[0] // we read the values as string | number.
    // An input array of string[] or number[] is safe to read from
    // An input array of (string | number | boolean)[] would be unsafe, as we might read a boolean, in breach of the constraint

    array.push(1) // we can write values of string | number
    array.push('foo') // we can write values of string | number
    // an input array of (string | number | boolean)[]) is safe to write to
    // but an input array of number[] is unsafe to write to, because we might write a string

    // ergo, the only safe input array is the exact same type: (string | number)[]
}
stuck cape
#

So the only type that can be used is a type that is an exact match

high pelican
#

Right, that's why I got lost with the whole direction thing on the cb lol

earnest frigateBOT
#
sandiford#0

Preview:```ts
export type BaseContext = object

// Internal state
export type BaseState = {
isEnabled?: boolean;
};

// User provided state
export type ExtendedState = object;
// Or I could enforce the user has specific properties in here as well
export type State<TExtendedState extends ExtendedState> = BaseState & TExtendedState;
...```

high pelican
#

So I guess the question is, how do I maintain a narrow enough type where I need it, so the user can use the callback correctly, and my Features container can safely call stuff like set({ isEnabled: false }) that it knows about, etc?

stuck cape
#

You basically need to use any for that type param

#

The only fully typed way to do it is to have only a single TExtendedState in your map

////////////////////////////////////////
// Using a specific type
////////////////////////////////////////
const featuresMap = {
  feature,
  feature2,
} satisfies Record<string, Feature<BaseContext, ExampleState>>;
#

this means when we access featuresMap, we have an object that we can use as Feature<BaseContext, ExampleState>

high pelican
#

So explain to me like I'm 5, what the difference here would be using any vs never vs unknown in satisfies FeaturesMap<Context, ...>

stuck cape
#

unknown is too wide for your constraint of extends ExtendedState, so you can't use it

#

Your widest type is ExtendedState

#

Your narrowest type is never

high pelican
#

Is any wider?

stuck cape
#

any is... magic

#

like do anything you want

#

it's not a type that follows the rules

high pelican
#

Gotcha, that's why people gripe about it all the time lol

stuck cape
#

yeah

high pelican
#

I still need to research never more, since I never knew what it did lol

stuck cape
#

It's just an empty type that can be assigned to anything

#

But yeah you might have to play with it to understand it

high pelican
#

So why wouldn't never work here if it's narrower than ExtendedState?

stuck cape
#

Because you have ExtendedState in both the sending a receiving position

#

so your feature instances have to be an exact match to the type specified

high pelican
#

Ok

stuck cape
#

So your callback arg would have to be never and it would have to return never

#

which is not exactly useful

high pelican
#

Not likely lol

stuck cape
#

If you were to do

const featuresMap: Record<string, Feature<BaseContext, any>> = {
  feature,
  feature2,
};

This is pretty bad, because you've basically just turned off type checking of TExtendState

high pelican
#

Ok, I'm gonna go play with it and see if I can fix all my code lol. Don't be surpised if I come back crying 😂

stuck cape
#

But you can do

const featuresMap = {
  feature,
  feature2,
} as const satisfies Record<string, Feature<BaseContext, any>>;
high pelican
#

Hmm

stuck cape
#

Because we're not taking any into our final type, its only used in the constraint

high pelican
#

Right, the former would probably break all my state inferences downstream when accessing those instances, right?

stuck cape
#

if feature and feature2 have different TExtendedState types, they are basically different and incompatible types, there's no way to make a homogenous record with them

stuck cape
#

probably all the types depending on TExtendState would just become any

high pelican
#

Any soup lol

stuck cape
#

and all the type checking woudl be disabled

stuck cape
#

That's a good way to put it 🙂

high pelican
#

Ok, at least my assumption was correct. I understand SOME things 😂

#

Ok, I have to dip for a bit, but I will pop back in after I refactor today. I hugely appreciate your help.

#

If you have a coffee fund button, lmk 😂

stuck cape
#

Not really, don't worry 😆

earnest frigateBOT
#
sandiford#0

Preview:```ts
export type BaseContext = object

// Internal state
export type BaseState = {
isEnabled?: boolean;
};

// User provided state
export type ExtendedState = object;
// Or I could enforce the user has specific properties in here as well
export type State<TExtendedState extends ExtendedState> = BaseState & TExtendedState;
...```

stuck cape
#

Here's the state of my TSP if you need it. More or less the same as the last one.

high pelican
#

What is this? lol type T = never extends any ? true : false

stuck cape
#

oh I forgot that

#

I was just testing

high pelican
#

Haha

#

I'm also running into the same callback issue with my Context type, so I'm seeing if I can figure out before I annoy you

high pelican
#

I have a bunch of stuff working, but it appears I lost my type inference in the featuresMap whne calling features.getFeatures() or features.getFeature()

#

I'm tinkering with it for a min, then I'll send over a minimal example if I can't get it working

#

Oh poop, I figured it out. It's cuz I'm passing <Context> and not TFeaturesMap, and supplying only some genrerics breaks all of them. I have to explicitly pass it in like so:

makeFeatures<Context, typeof featuresMap>({
  featuresMap,
});

Do you know a way around this?

#

I know xstate does some magic like so, so that might be one way:

makeFeatures({
  types: {} as {
    context: Context,
  },
  featuresMap,
})
#

Also, this is why I got confused about the extra properties:

export type Config<
  TContext extends Context = Context,
  TFeatureState extends FeatureState = FeatureState
> = {
  initialState: TFeatureState;
  storage: {
    key: string;
    version?: string;
  };
  isEnabled?: (args: {
    context: TContext;
    state: TFeatureState;
    isEnabled?: InternalState["isEnabled"];
  }) => boolean;
  renderDevTools?: (/*renderProps: ...*/) => ReactNode;
};

const feature = makeFeature<Context, MAWState>({
  initialState: {
    testExtendedStateProp: false,
  },
  storage: {
    key: `multi-account-warning`,
    version: `1.0.0`,
  },
  isEnabled: ({ context, state, isEnabled }) => {
    return (
      // (context.user.betaFeaturesEnabled ||
      //   context.device.betaFeaturesEnabled) &&
      isEnabled !== false
    );
  },
  renderDevTools: () => <RenderFeature />,

  // Error: Object literal may only specify known properties, and customMethod does not exist 
  // in type Config<TContext extends Context = Context, TFeatureState extends FeatureState = FeatureState>
  customMethod: () => {},
});

Lmk what you think 🙂

stuck cape
#

The only way to avoid that is to break it up in a sequence of calls makeFeatures<Context>()()

#

notice double ()

#

The first function takes 1 param and returns a new function with 1 param that infers

stuck cape
#

// Error: Object literal may only specify known properties, and customMethod does not exist

#

If you create an object literal in a place where the extra properties can never be accessed, you get errors about excess properties

#

In theory anyway

#

I got confused by that too

high pelican
#

Ok, I will look at that second part after I'm done fixing the stuff I just broke 5min ago too, haha

stuck cape
#

If you get a false positive, create the object as a standalone first, then pass it to the function

#
const featureData = {
  initialState: {
    testExtendedStateProp: false,
  },
  storage: {
    key: `multi-account-warning`,
    version: `1.0.0`,
  },
  isEnabled: ({ context, state, isEnabled }) => {
    return (
      // (context.user.betaFeaturesEnabled ||
      //   context.device.betaFeaturesEnabled) &&
      isEnabled !== false
    );
  },
  renderDevTools: () => <RenderFeature />,

  // Error: Object literal may only specify known properties, and customMethod does not exist 
  // in type Config<TContext extends Context = Context, TFeatureState extends FeatureState = FeatureState>
  customMethod: () => {},
} // satisfies something?

const feature = makeFeature<Context, MAWState>(featureData);
high pelican
#

Actually, I'm just an idiot lol. I was trying to add the custom method to the config object. Creating an extended class works just fine. I should stop coding while drinking maybe? 🥳

high pelican