S
SolidJS6mo ago
jack

type narrowing signals

I've got some state (I'm making a card game) that looks like
type GameInProgress = {
state: "playing" | "paused";
p1Flipped: boolean;
p2Flipped: boolean;
};

type GameOver = {
state: "over";
};

type GamePaused = {
state: "paused";
p1Flipped: true;
p2Flipped: true;
};

type Game = {
state: "playing" | "paused" | "over";
} & (GameInProgress | GamePaused | GameOver);
type GameInProgress = {
state: "playing" | "paused";
p1Flipped: boolean;
p2Flipped: boolean;
};

type GameOver = {
state: "over";
};

type GamePaused = {
state: "paused";
p1Flipped: true;
p2Flipped: true;
};

type Game = {
state: "playing" | "paused" | "over";
} & (GameInProgress | GamePaused | GameOver);
and then my signal to track game state ends up looking like
const [gameState, setGameState] = createSignal<Game>({
state: "over",
});
const [gameState, setGameState] = createSignal<Game>({
state: "over",
});
over time, i want to update this object. since i'm using discriminated types, if i want to reason about any of the props that only exist on GamePaused or GameOver, I need to do some narrowing. it seems like doing a if (gameState().state !== "playing") { return } ... gameState().___ doesn't successfully autocomplete. I'm assuming that typescript doesn't have enough info to guarantee that the 2nd getter invocation will be equivalent to that of the first. Next reasonable idea is to invoke the getter once and store in a variable, then use that to type narrow. But then when I do a setGameState(...) using that variable, I'm not following the nested reactivity pattern here https://www.solidjs.com/tutorial/stores_nested_reactivity Any thoughts/best practices here ?
10 Replies
REEEEE
REEEEE6mo ago
Could you give an example of what you mean by "nested reactivity pattern"? The way you are doing it by assigning to a variable to help narrowing typescript is completely fine. The tutorial is an introduction to stores which essentially create signals for each property access internally so that you won't have to do so manually for each nested property you want to be reactive
jack
jack6mo ago
I just noticed that the example called the signal directly inside the side, as opposed to creating a variable, that's what I was referring to But if it doesn't matter, then ok that's cool. I'm running into some ugly typescript issues then it looks like What about when I have something like
type GameInProgress = {
p1Flipped: Signal<boolean>;
p2Flipped: Signal<boolean>;
} & (
| {
state: Accessor<"playing">;
setState: Setter<"playing">;
}
| {
state: Accessor<"paused">;
setState: Setter<"paused">;
}
);
type GameInProgress = {
p1Flipped: Signal<boolean>;
p2Flipped: Signal<boolean>;
} & (
| {
state: Accessor<"playing">;
setState: Setter<"playing">;
}
| {
state: Accessor<"paused">;
setState: Setter<"paused">;
}
);
Is there anyway to narrow such that if we know state is Accessor<"playing">, then we will know that setState is Setter<"playing">? I'm trying some hacky things to get it going, but am getting that my setState is not callable This expression is not callable.Each member of the union type 'Setter<"playing"> | Setter<"paused"> | Setter<"over">' has signatures, but none of those signatures are compatible with each other. I think it's not narrowing successfully is the issuee but can't find a solution.
const fn = () => {
const currentState = gameState().state();
if (currentState !== "playing") return;

gameState().setState("paused");
}
const fn = () => {
const currentState = gameState().state();
if (currentState !== "playing") return;

gameState().setState("paused");
}
doesn't do anything for me really
REEEEE
REEEEE6mo ago
I think for your Game type definition it doesn't need state. Just a union of GameInProgress | GamePaused | GameOver should be enough to define Game
jack
jack6mo ago
Sorry, 1 sec, I've overhauled the types a fair bit this is what i changed it to
type GameInProgress = {
state: Accessor<"playing"> | Accessor<"paused">;
setState: Setter<"playing"> | Setter<"paused">;
p1Flipped: Signal<boolean>;
p2Flipped: Signal<boolean>;
} & (
| {
state: Accessor<"playing">;
setState: Setter<"playing">;
}
| {
state: Accessor<"paused">;
setState: Setter<"paused">;
}
);

type GameOver = {
state: Accessor<"over">;
setState: Setter<"over">;
};

type Game = GameInProgress | GameOver;
type GameInProgress = {
state: Accessor<"playing"> | Accessor<"paused">;
setState: Setter<"playing"> | Setter<"paused">;
p1Flipped: Signal<boolean>;
p2Flipped: Signal<boolean>;
} & (
| {
state: Accessor<"playing">;
setState: Setter<"playing">;
}
| {
state: Accessor<"paused">;
setState: Setter<"paused">;
}
);

type GameOver = {
state: Accessor<"over">;
setState: Setter<"over">;
};

type Game = GameInProgress | GameOver;
REEEEE
REEEEE6mo ago
Also in this case maybe separating gameState into it's own variable first will work better
const gs = gameState()
const currentState = gs.state()
...

gs.setState
const gs = gameState()
const currentState = gs.state()
...

gs.setState
jack
jack6mo ago
yea i've tried that. it doesn't affect the setter at all though
REEEEE
REEEEE6mo ago
hmm not too sure, is there a reason for those values to be accessor and setters? They are only storing one value based on the types
Brendonovich
Brendonovich6mo ago
I'd recommend just assigning the result of gameState to a separate value and using that. Doing fancy types and stuff with the setter and adding more accessors won't help since you still need to discriminate between the different accessors.
type GameInProgress = {
state: "playing" | "paused";
p1Flipped: boolean;
p2Flipped: boolean;
};

type GameOver = { state: "over" };

type Game = GameInProgress | GameOver;

const [getGameState, setGameState] = createSignal<Game>({
state: "over",
});

createEffect(() => {
const gameState = getGameState();
if(gameState.state !== "playing") return;

// use type-narrowed gameState...
})

return (
<Switch>
<Match when={(() => {
const gameState = getGameState();
if(gameState.state === "playing") return gameState;
})()}>
// narrowed
{gameState => ...}
</Match>
<Match when={(() => {
const gameState = getGameState();
if(gameState.state === "over") return gameState;
})()}>
// narrowed
{gameState => ...}
</Match>
</Switch>
)
type GameInProgress = {
state: "playing" | "paused";
p1Flipped: boolean;
p2Flipped: boolean;
};

type GameOver = { state: "over" };

type Game = GameInProgress | GameOver;

const [getGameState, setGameState] = createSignal<Game>({
state: "over",
});

createEffect(() => {
const gameState = getGameState();
if(gameState.state !== "playing") return;

// use type-narrowed gameState...
})

return (
<Switch>
<Match when={(() => {
const gameState = getGameState();
if(gameState.state === "playing") return gameState;
})()}>
// narrowed
{gameState => ...}
</Match>
<Match when={(() => {
const gameState = getGameState();
if(gameState.state === "over") return gameState;
})()}>
// narrowed
{gameState => ...}
</Match>
</Switch>
)
jack
jack6mo ago
I had something much like this originally, any time p1Flipped or p2Flipped are updated, the ui that depends on either of them are both updated. That's why I moved to using signals inside the state object
Brendonovich
Brendonovich6mo ago
In that case wouldn't a store be better?
const [gameState, setGameState] = createStore<Game>({
state: "over",
});

setGameState(
// function necessary to replace whole store
() => ({
state: "playing",
p1Flipped: true,
p2Flipped: false
}),
);
const [gameState, setGameState] = createStore<Game>({
state: "over",
});

setGameState(
// function necessary to replace whole store
() => ({
state: "playing",
p1Flipped: true,
p2Flipped: false
}),
);