233 Replies
whatplan
whatplan•12mo ago
I am willing to help if you provide further explanation or something
why is scoreToWin is available here?
is not enough to help
Tom
Tom•12mo ago
the idea behind the example is that im trying to make it so that GameModel has a scoreToWin if TScoring is NumericScoreSystem. If its not a NumericScoreSystem I would expect scoreToWin to not be present but in the code i gave GameModel should be using the union type ScoreSystem which seems to me like it should not extend NumericScoreSystem so my question is ' why doesn't ts complain when i try to set scoreToWin?'
whatplan
whatplan•12mo ago
so your right on the union
whatplan
whatplan•12mo ago
ill work on the rest
Tom
Tom•12mo ago
wdym it will work on the rest?
whatplan
whatplan•12mo ago
I will take a look at the rest*
Tom
Tom•12mo ago
oh ok. ty
whatplan
whatplan•12mo ago
can you show an example of how you would use it if the types worked as you want? im confused why the generic is even really necessary given its only used currently as a true false
export type GameModel<T extends boolean = false> = {
id: string;
} & (T extends true ? { scoreToWin: number } : Record<never, never>);

const a: GameModel = {
id: 'a',
scoreToWin: 1, //this errors correctly
}
export type GameModel<T extends boolean = false> = {
id: string;
} & (T extends true ? { scoreToWin: number } : Record<never, never>);

const a: GameModel = {
id: 'a',
scoreToWin: 1, //this errors correctly
}
like this would work
Tom
Tom•12mo ago
i have a much larger heirarchy of types. this is just the bottom
whatplan
whatplan•12mo ago
I see
Tom
Tom•12mo ago
it also makes me think that i dont understand something basic (which is definitely true. im not very good at the really deep ts) it seems like you might have some experience here so can i ask: in your opinion, is it worth it to go this heavy on the typing?
Tom
Tom•12mo ago
im doing all this cause my app had a union of 2 very similar types and i needed to add a 3rd. then i realized that i could make my typing much more strict everywhere but this is.... tough. and its not really making things more readable
whatplan
whatplan•12mo ago
the way I would think about it is types are a great way to understand what you want your program to do before you even write it you can start out zero code, just type signatures and plan out the flow of data through your entire app but for any given app there are multiple ways to describe the types if you want to share more of your code Id be happy you help you think it through
Tom
Tom•12mo ago
i would actually really appreciate that ill show you what i have. im still in the middle of writing it, but im having a lot of second thoughts it feels like its getting really complex and part of it just is cause it is complex, but im like turning all my core logic into generics and stuff, which is causing other issues
whatplan
whatplan•12mo ago
real apps are complex and require complex types to describe their states but I guarantee having complex, but robust types will be much better than trying to hold everything in your head
Tom
Tom•12mo ago
here are the 2 key files ive been working on
whatplan
whatplan•12mo ago
ok gimme a bit to go through
Tom
Tom•12mo ago
np. take your time really appreciate it here's an example where i feel like im gunna have to make things a lot more complex / less readable for the sake of the typing:
function applyBracketMetadata<TScoring extends ScoreSystem>(
bracket: BracketModel<TScoring>,
games: GameModel<TScoring>[]
) {
let currentHumanGameID = 0;

for (let i = 0; i < games.length; i++) {
const game = games[i];
const round = bracket.rounds[game.round];

/* add score to win based on the round */
// <---------------------------------
// This is now an error because i used the type used to just be 'scoreToWin?: number'
// but now its correctly checking against TScoring. But I can't test if TScoring is
// NumericScoreSystem at runtime. I could add discriminators all over the place, but
// that seems like it would be a crazy number of discriminators given my type heirarchy.
// Is there a better way to do this?
game.scoreToWin = round.scoreToWin;

/* add the game name based on the bracket ID, round number, and incrementing ID */
//...
}
}
function applyBracketMetadata<TScoring extends ScoreSystem>(
bracket: BracketModel<TScoring>,
games: GameModel<TScoring>[]
) {
let currentHumanGameID = 0;

for (let i = 0; i < games.length; i++) {
const game = games[i];
const round = bracket.rounds[game.round];

/* add score to win based on the round */
// <---------------------------------
// This is now an error because i used the type used to just be 'scoreToWin?: number'
// but now its correctly checking against TScoring. But I can't test if TScoring is
// NumericScoreSystem at runtime. I could add discriminators all over the place, but
// that seems like it would be a crazy number of discriminators given my type heirarchy.
// Is there a better way to do this?
game.scoreToWin = round.scoreToWin;

/* add the game name based on the bracket ID, round number, and incrementing ID */
//...
}
}
whatplan
whatplan•12mo ago
can you describe like what your goal is is the idea to have a bunch of funcitons that are generic over different ScoreSystems?
Tom
Tom•12mo ago
yeah. kinda ok so im gunna simplify here a lot but a good example is that the scoring logic is only actually used in like 3 places. most of the logic is about figuring out who is going to play who,managing game states, etc all of those functions just want to figure out who won and they dont care whether the win was using the annotated score syem or the numeric one and to make those terms a little more concrete. the numeric score system is a score like 3 - 5 the annotated score system is like 'this guy won by knock out' which gets written down as 'KO' - 'L'
whatplan
whatplan•12mo ago
ok so
all of those functions just want to figure out who won and they dont care whether the win was using the annotated score syem or the numeric one
Tom
Tom•12mo ago
but again, the rest of the code just cares about knowing who won
whatplan
whatplan•12mo ago
does what they do change based on the type of the scoring system
Tom
Tom•12mo ago
no thats kind of the point
whatplan
whatplan•12mo ago
so what is the like "shared api" between the scoring systems
Tom
Tom•12mo ago
well in my old code. the type of score was literally like score: number | LossReason | WinReason
whatplan
whatplan•12mo ago
oh I think I see like all of the models are the apis
Tom
Tom•12mo ago
yeah. the models tell you how to apply each score system the app makes this UI
Tom
Tom•12mo ago
whatplan
whatplan•12mo ago
hm ok gimme a sec
Tom
Tom•12mo ago
and so i look at the format to tell me what score each player needs in order to win or if theyre using the annotated version i dont need that thats what im trying to describe in the types take your time. tyvm for looking
whatplan
whatplan•12mo ago
ok so everything is being wrapped around the ScoreSystem type but your saying thats only used in like a few functions that actually calculate win/loss can you send those functions
Tom
Tom•12mo ago
yeah 1 sec its a little long, but this is like the key one
function checkScoreProgress(tournament: TournamentModel, game: GameModel): GameProgressInfo {
/*
* Check that the game is progressable. Some functions that
* call this are simply looping through a list of game slots
* that have been updated. This check prevents us from redoing
* work and ensures we dont double-count wins.
*/
if (!gameUtils.isActionableState(game.state)) return { advancements: [], gameDone: false };

/* check for loss conditions */
for (let i = 0; i < game.slots.length; i++) {
const slot = game.slots[i];
const opponentSlot = game.slots[(i + 1) % 2];
const player = tournamentUtils.getPlayerByID(tournament, slot.playerID!)!;

if (
gameSlotUtils.isBye(slot) ||
(player.isDisqualified && !gameSlotUtils.isBye(opponentSlot)) ||
scoreUtils.scoreIsLoss(slot.score)
) {
/* for lose conditions we assign a winner without awarding wins / losses */
slot.isWinner = false;
opponentSlot.isWinner = true;
game.state = GameState.DONE;
if (player.isDisqualified) slot.score = "DQ";

/* perform special case handling if this is grand finals */
if (tournamentUtils.isDoubleElim(tournament) && game.id === "W-GF") {
checkGrandFinals(tournament, game);
}

const advancements = [];
if (game.nextGameSlotIDs?.[0]) {
advancements.push({
playerID: opponentSlot.playerID!,
nextGameSlotID: game.nextGameSlotIDs[0],
});
}
if (game.nextGameSlotIDs?.[1]) {
advancements.push({
playerID: slot.playerID!,
nextGameSlotID: game.nextGameSlotIDs[1],
});
}
return { advancements, gameDone: true };
}
}

/* check for win conditions */
for (let i = 0; i < game.slots.length; i++) {
const slot = game.slots[i];
const opponentSlot = game.slots[(i + 1) % 2];

if (typeof slot.score === "number" && slot.score >= game.scoreToWin) { // <---------this is an error now
/* assign the winner and increment win / loss records */
slot.isWinner = true;
opponentSlot.isWinner = false;
game.state = GameState.DONE;

/* perform special case handling if this is grand finals */
if (tournamentUtils.isDoubleElim(tournament) && game.id === "W-GF")
checkGrandFinals(tournament, game);

/* Exhibition games don't count towards wins / losses */
if (!gameUtils.isExhibition(game)) {
if (slot.playerID) {
const player = tournamentUtils.getPlayerByID(tournament, slot.playerID)!;
player.wins++;
}
opponentSlot.isWinner = false;
if (opponentSlot.playerID) {
const opponent = tournamentUtils.getPlayerByID(tournament, opponentSlot.playerID)!;
opponent.losses++;
}
}

const advancements = [];
if (game.nextGameSlotIDs?.[0]) {
advancements.push({
playerID: slot.playerID!,
nextGameSlotID: game.nextGameSlotIDs[0],
});
}
if (game.nextGameSlotIDs?.[1]) {
advancements.push({
playerID: opponentSlot.playerID!,
nextGameSlotID: game.nextGameSlotIDs[1],
});
}
return { advancements, gameDone: true };
}
}

return { advancements: [], gameDone: false };
}
function checkScoreProgress(tournament: TournamentModel, game: GameModel): GameProgressInfo {
/*
* Check that the game is progressable. Some functions that
* call this are simply looping through a list of game slots
* that have been updated. This check prevents us from redoing
* work and ensures we dont double-count wins.
*/
if (!gameUtils.isActionableState(game.state)) return { advancements: [], gameDone: false };

/* check for loss conditions */
for (let i = 0; i < game.slots.length; i++) {
const slot = game.slots[i];
const opponentSlot = game.slots[(i + 1) % 2];
const player = tournamentUtils.getPlayerByID(tournament, slot.playerID!)!;

if (
gameSlotUtils.isBye(slot) ||
(player.isDisqualified && !gameSlotUtils.isBye(opponentSlot)) ||
scoreUtils.scoreIsLoss(slot.score)
) {
/* for lose conditions we assign a winner without awarding wins / losses */
slot.isWinner = false;
opponentSlot.isWinner = true;
game.state = GameState.DONE;
if (player.isDisqualified) slot.score = "DQ";

/* perform special case handling if this is grand finals */
if (tournamentUtils.isDoubleElim(tournament) && game.id === "W-GF") {
checkGrandFinals(tournament, game);
}

const advancements = [];
if (game.nextGameSlotIDs?.[0]) {
advancements.push({
playerID: opponentSlot.playerID!,
nextGameSlotID: game.nextGameSlotIDs[0],
});
}
if (game.nextGameSlotIDs?.[1]) {
advancements.push({
playerID: slot.playerID!,
nextGameSlotID: game.nextGameSlotIDs[1],
});
}
return { advancements, gameDone: true };
}
}

/* check for win conditions */
for (let i = 0; i < game.slots.length; i++) {
const slot = game.slots[i];
const opponentSlot = game.slots[(i + 1) % 2];

if (typeof slot.score === "number" && slot.score >= game.scoreToWin) { // <---------this is an error now
/* assign the winner and increment win / loss records */
slot.isWinner = true;
opponentSlot.isWinner = false;
game.state = GameState.DONE;

/* perform special case handling if this is grand finals */
if (tournamentUtils.isDoubleElim(tournament) && game.id === "W-GF")
checkGrandFinals(tournament, game);

/* Exhibition games don't count towards wins / losses */
if (!gameUtils.isExhibition(game)) {
if (slot.playerID) {
const player = tournamentUtils.getPlayerByID(tournament, slot.playerID)!;
player.wins++;
}
opponentSlot.isWinner = false;
if (opponentSlot.playerID) {
const opponent = tournamentUtils.getPlayerByID(tournament, opponentSlot.playerID)!;
opponent.losses++;
}
}

const advancements = [];
if (game.nextGameSlotIDs?.[0]) {
advancements.push({
playerID: slot.playerID!,
nextGameSlotID: game.nextGameSlotIDs[0],
});
}
if (game.nextGameSlotIDs?.[1]) {
advancements.push({
playerID: opponentSlot.playerID!,
nextGameSlotID: game.nextGameSlotIDs[1],
});
}
return { advancements, gameDone: true };
}
}

return { advancements: [], gameDone: false };
}
whatplan
whatplan•12mo ago
im missing GameModel and TournamentModel from what I have would be helpful
Tom
Tom•12mo ago
yeah 1 sec
whatplan
whatplan•12mo ago
no worries
Tom
Tom•12mo ago
export type GameModel<TScoring extends ScoreSystem = ScoreSystem> = {
id: string;
name: string;
round: number;
slots: GameSlotModel<TScoring>[];
state: GameState;
activeSince: number | null;
availableSince: number | null;
calledSince: number | null;
endTime: number | null;
nextGameSlotIDs: string[] | null;
locationID: string | null;
loserPlacement: number | null;
} & (TScoring extends NumericScoreSystem ? { scoreToWin: number } : Record<never, never>);
export type GameModel<TScoring extends ScoreSystem = ScoreSystem> = {
id: string;
name: string;
round: number;
slots: GameSlotModel<TScoring>[];
state: GameState;
activeSince: number | null;
availableSince: number | null;
calledSince: number | null;
endTime: number | null;
nextGameSlotIDs: string[] | null;
locationID: string | null;
loserPlacement: number | null;
} & (TScoring extends NumericScoreSystem ? { scoreToWin: number } : Record<never, never>);
whatplan
whatplan•12mo ago
cool gimme a bit
Tom
Tom•12mo ago
and then TournamentModel is the big one the whole app is built to work against the tournament model which didnt used to be generic but here it is now
whatplan
whatplan•12mo ago
what im thinking
Tom
Tom•12mo ago
type TournamentModelImpl<
TScoring extends ScoreSystem,
TFormat extends FormatModel<TScoring>,
TFormatOpts extends FormatOptionsModel<TScoring>
> = TournamentModelFields & {
format: TFormat;
formatOptions: TFormatOpts;
games: GameModel<TScoring>[];
};

export type AnnotatedSingleElimTournamentModel = TournamentModelImpl<
AnnotatedScoreSystem,
SingleElimModel<AnnotatedScoreSystem>,
SingleElimOptionsModel<AnnotatedScoreSystem>
>;
export type AnnotatedDoubleElimTournamentModel = TournamentModelImpl<
AnnotatedScoreSystem,
DoubleElimModel<AnnotatedScoreSystem>,
DoubleElimOptionsModel<AnnotatedScoreSystem>
>;
export type AnnotatedBracketedTournamentModel =
| AnnotatedSingleElimTournamentModel
| AnnotatedDoubleElimTournamentModel;
export type AnnotatedRoundRobinTournamentModel = TournamentModelImpl<
AnnotatedScoreSystem,
RoundRobinModel<AnnotatedScoreSystem>,
RoundRobinOptionsModel<AnnotatedScoreSystem>
>;

export type NumericSingleElimTournamentModel = TournamentModelImpl<
NumericScoreSystem,
SingleElimModel<NumericScoreSystem>,
SingleElimOptionsModel<NumericScoreSystem>
>;
export type NumericDoubleElimTournamentModel = TournamentModelImpl<
NumericScoreSystem,
DoubleElimModel<NumericScoreSystem>,
DoubleElimOptionsModel<NumericScoreSystem>
>;
export type NumericBracketedTournamentModel =
| NumericSingleElimTournamentModel
| NumericDoubleElimTournamentModel;
export type NumericRoundRobinTournamentModel = TournamentModelImpl<
NumericScoreSystem,
RoundRobinModel<NumericScoreSystem>,
RoundRobinOptionsModel<NumericScoreSystem>
>;

export type NumericTournamentModel = TournamentModelImpl<
NumericScoreSystem,
FormatModel<NumericScoreSystem>,
FormatOptionsModel<NumericScoreSystem>
>;

export type AnnotatedTournamentModel = TournamentModelImpl<
AnnotatedScoreSystem,
FormatModel<AnnotatedScoreSystem>,
FormatOptionsModel<AnnotatedScoreSystem>
>;

export type SingleElimTournamentModel<TScoring extends ScoreSystem> = TournamentModelImpl<
TScoring,
SingleElimModel<TScoring>,
SingleElimOptionsModel<TScoring>
>;
export type DoubleElimTournamentModel<TScoring extends ScoreSystem> = TournamentModelImpl<
TScoring,
DoubleElimModel<TScoring>,
DoubleElimOptionsModel<TScoring>
>;
export type RoundRobinTournamentModel<TScoring extends ScoreSystem> = TournamentModelImpl<
TScoring,
RoundRobinModel<TScoring>,
RoundRobinOptionsModel<TScoring>
>;

export type BracketedTournamentModel<TScoring extends ScoreSystem> = TournamentModelImpl<
TScoring,
BracketedFormatModel<TScoring>,
BracketedFormatOptionsModel<TScoring>
>;

export type TournamentModel<
TScoring extends ScoreSystem = ScoreSystem,
TFormat extends FormatModel<TScoring> = FormatModel<TScoring>,
TFormatOpts extends FormatOptionsModel<TScoring> = FormatOptionsModel<TScoring>
> = TournamentModelImpl<TScoring, TFormat, TFormatOpts>;
type TournamentModelImpl<
TScoring extends ScoreSystem,
TFormat extends FormatModel<TScoring>,
TFormatOpts extends FormatOptionsModel<TScoring>
> = TournamentModelFields & {
format: TFormat;
formatOptions: TFormatOpts;
games: GameModel<TScoring>[];
};

export type AnnotatedSingleElimTournamentModel = TournamentModelImpl<
AnnotatedScoreSystem,
SingleElimModel<AnnotatedScoreSystem>,
SingleElimOptionsModel<AnnotatedScoreSystem>
>;
export type AnnotatedDoubleElimTournamentModel = TournamentModelImpl<
AnnotatedScoreSystem,
DoubleElimModel<AnnotatedScoreSystem>,
DoubleElimOptionsModel<AnnotatedScoreSystem>
>;
export type AnnotatedBracketedTournamentModel =
| AnnotatedSingleElimTournamentModel
| AnnotatedDoubleElimTournamentModel;
export type AnnotatedRoundRobinTournamentModel = TournamentModelImpl<
AnnotatedScoreSystem,
RoundRobinModel<AnnotatedScoreSystem>,
RoundRobinOptionsModel<AnnotatedScoreSystem>
>;

export type NumericSingleElimTournamentModel = TournamentModelImpl<
NumericScoreSystem,
SingleElimModel<NumericScoreSystem>,
SingleElimOptionsModel<NumericScoreSystem>
>;
export type NumericDoubleElimTournamentModel = TournamentModelImpl<
NumericScoreSystem,
DoubleElimModel<NumericScoreSystem>,
DoubleElimOptionsModel<NumericScoreSystem>
>;
export type NumericBracketedTournamentModel =
| NumericSingleElimTournamentModel
| NumericDoubleElimTournamentModel;
export type NumericRoundRobinTournamentModel = TournamentModelImpl<
NumericScoreSystem,
RoundRobinModel<NumericScoreSystem>,
RoundRobinOptionsModel<NumericScoreSystem>
>;

export type NumericTournamentModel = TournamentModelImpl<
NumericScoreSystem,
FormatModel<NumericScoreSystem>,
FormatOptionsModel<NumericScoreSystem>
>;

export type AnnotatedTournamentModel = TournamentModelImpl<
AnnotatedScoreSystem,
FormatModel<AnnotatedScoreSystem>,
FormatOptionsModel<AnnotatedScoreSystem>
>;

export type SingleElimTournamentModel<TScoring extends ScoreSystem> = TournamentModelImpl<
TScoring,
SingleElimModel<TScoring>,
SingleElimOptionsModel<TScoring>
>;
export type DoubleElimTournamentModel<TScoring extends ScoreSystem> = TournamentModelImpl<
TScoring,
DoubleElimModel<TScoring>,
DoubleElimOptionsModel<TScoring>
>;
export type RoundRobinTournamentModel<TScoring extends ScoreSystem> = TournamentModelImpl<
TScoring,
RoundRobinModel<TScoring>,
RoundRobinOptionsModel<TScoring>
>;

export type BracketedTournamentModel<TScoring extends ScoreSystem> = TournamentModelImpl<
TScoring,
BracketedFormatModel<TScoring>,
BracketedFormatOptionsModel<TScoring>
>;

export type TournamentModel<
TScoring extends ScoreSystem = ScoreSystem,
TFormat extends FormatModel<TScoring> = FormatModel<TScoring>,
TFormatOpts extends FormatOptionsModel<TScoring> = FormatOptionsModel<TScoring>
> = TournamentModelImpl<TScoring, TFormat, TFormatOpts>;
whatplan
whatplan•12mo ago
is the entire tree of types doesnt need to be generic so lemme see what I can do
Tom
Tom•12mo ago
that would be great 😄 the problem with that is that the scoreToWin is stored in the RoundModel but maybe i could simplify things a lot if i just kinda 'denormalize' it and put the score directly in the games
whatplan
whatplan•12mo ago
just lemme think before you make any big changes theres a lot to look at
Tom
Tom•12mo ago
haha np. yeah i mean. this is all like in the middle of the change for me its been a long 2 days 😛 thanks again. really appreciate it
whatplan
whatplan•12mo ago
np need GameSlotModel
Tom
Tom•12mo ago
export type GameSlotModel<TScoring extends ScoreSystem = ScoreSystem> = {
gameID: string;
slotIdx: number;
playerID: string | null;
checkInTime: number | null;
waitingTime: number | null;
prevGameID: string | null;
score: TScoring extends NumericScoreSystem ? NumericScore : AnnotatedScore;
isWinner: boolean;
};
export type GameSlotModel<TScoring extends ScoreSystem = ScoreSystem> = {
gameID: string;
slotIdx: number;
playerID: string | null;
checkInTime: number | null;
waitingTime: number | null;
prevGameID: string | null;
score: TScoring extends NumericScoreSystem ? NumericScore : AnnotatedScore;
isWinner: boolean;
};
i think that should be the last thing you could want
whatplan
whatplan•12mo ago
should be
Tom
Tom•12mo ago
it would be hard to do this though.... the user wants to specify it per-round. This comes in thru the RoundOptionsModel so thats probably going to need to be capable of supplying it...
whatplan
whatplan•12mo ago
type TournamentModelImpl<
TScoring extends ScoreSystem,
TFormat extends FormatModel<TScoring>,
TFormatOpts extends FormatOptionsModel<TScoring>
> = TournamentModelFields & {
format: TFormat;
formatOptions: TFormatOpts;
games: GameModel<TScoring>[];
};
type TournamentModelImpl<
TScoring extends ScoreSystem,
TFormat extends FormatModel<TScoring>,
TFormatOpts extends FormatOptionsModel<TScoring>
> = TournamentModelFields & {
format: TFormat;
formatOptions: TFormatOpts;
games: GameModel<TScoring>[];
};
if TournamentModel has a GameModel inside it why does that big function need both seperately?
Tom
Tom•12mo ago
the big function is given a game to check if it needs to progress. i could theoertically also just give that function a gameID and look it up but the function that calls this already has the updated game it needs the tournament as context, to look up the players so it can assign wins and losses and stuff
whatplan
whatplan•12mo ago
I need the types from this FormatModel FormatiOptionsModel TournamentModelFields im starting from the very top and reconstructing down also I do think a generic makes sense again but theres probabaly a better way than what you have right now
Tom
Tom•12mo ago
FormatModel and FormatOptions Model are in the FormatModel.ts i hgave at the beginning
whatplan
whatplan•12mo ago
oh my b
Tom
Tom•12mo ago
Tom
Tom•12mo ago
as for TournamentModelFields theres a ton of unrelated stuff in there if you need a placeholder you can just do:
type TournamentModelFields = {
id: string;
}
type TournamentModelFields = {
id: string;
}
no worries i would LOVE to see it
whatplan
whatplan•12mo ago
ok a couple questions do you see yourself adding scoresystems or is it forever these 2
Tom
Tom•12mo ago
pertty sure this is it. i do see more formats as a possibility but i cant think of other ways to score games
whatplan
whatplan•12mo ago
heres where Im at: there is a way to avoid the generics but it all just depends on how you want the data to be structred so what I mean by that is actually hm
Tom
Tom•12mo ago
thats where ive been for 2 days 😛
whatplan
whatplan•12mo ago
I mean heres the thing right now you have conditions at the highest levels like gamemodel all the way to the very bottom with roundoptionsmodel you cant do that without generics everywhere which isnt necessarily a bad thing hm ah this is hard
Tom
Tom•12mo ago
i know 😛
whatplan
whatplan•12mo ago
lemme work through a bit more
Tom
Tom•12mo ago
ok. i think for me like i could like with a lot of my code being generic but things like this:
function applyBracketMetadata<TScoring extends ScoreSystem>(
bracket: BracketModel<TScoring>,
games: GameModel<TScoring>[]
) {
let currentHumanGameID = 0;

for (let i = 0; i < games.length; i++) {
const game = games[i];
const round = bracket.rounds[game.round];

/* add score to win based on the round */
// <---------------------------------
// This is now an error because i used the type used to just be 'scoreToWin?: number'
// but now its correctly checking against TScoring. But I can't test if TScoring is
// NumericScoreSystem at runtime. I could add discriminators all over the place, but
// that seems like it would be a crazy number of discriminators given my type heirarchy.
// Is there a better way to do this?
game.scoreToWin = round.scoreToWin;

/* add the game name based on the bracket ID, round number, and incrementing ID */
//...
}
}
function applyBracketMetadata<TScoring extends ScoreSystem>(
bracket: BracketModel<TScoring>,
games: GameModel<TScoring>[]
) {
let currentHumanGameID = 0;

for (let i = 0; i < games.length; i++) {
const game = games[i];
const round = bracket.rounds[game.round];

/* add score to win based on the round */
// <---------------------------------
// This is now an error because i used the type used to just be 'scoreToWin?: number'
// but now its correctly checking against TScoring. But I can't test if TScoring is
// NumericScoreSystem at runtime. I could add discriminators all over the place, but
// that seems like it would be a crazy number of discriminators given my type heirarchy.
// Is there a better way to do this?
game.scoreToWin = round.scoreToWin;

/* add the game name based on the bracket ID, round number, and incrementing ID */
//...
}
}
i just straight up dont feel like i have a good solution for without just making the types crazy i would also prefer not to have generics if possible because i cant make a zod validator generic the only way i can think to that is dto have a function that returns a zod validator but that almost made me write the word 'Factory' in my code
whatplan
whatplan•12mo ago
what are you planning to use zod for
Tom
Tom•12mo ago
which is where i knew id gone too far
whatplan
whatplan•12mo ago
I agree lmao
Tom
Tom•12mo ago
The entire TournamentModel gets validated with zod. i use this for 2 things 1) opentapi-trpc requires it to autogenerate rest apis from my trpc routes which is SO convenient
whatplan
whatplan•12mo ago
the entire tournament model?
Tom
Tom•12mo ago
yes 2) when i migrate the db i sanity check all my transformations by running them thru zod
whatplan
whatplan•12mo ago
wait but then why not just do it all in zod and then zod infer the types
Tom
Tom•12mo ago
i was going to do that at some point, but i think of this code as my 'core library' i designed it to be dependency free and heavily tested so the rest of the code just basically looks at it and draws UI
whatplan
whatplan•12mo ago
can you send the zod schema you have for the entire tournament model I wanna see that
Tom
Tom•12mo ago
right now i have duplicate zod types for everything
whatplan
whatplan•12mo ago
I understand this but it doesnt make a ton of sense
Tom
Tom•12mo ago
yeah but it will be the old model. i hadnt gotten around to updating it yet. its similar
whatplan
whatplan•12mo ago
if your using zod
Tom
Tom•12mo ago
yeah its a little annoying. the project was written before i knew about zod though and its all typesafe because the zod validators know if theyre out of sync with the model but it is the same types in 2 places
whatplan
whatplan•12mo ago
because heres the thing right like
Tom
Tom•12mo ago
Tom
Tom•12mo ago
theres a lot in there, but thatll give you the idea these validators are only used for checking output types so i didnt add all of the checks / restrictions to them the input parameters have much more strict checks
whatplan
whatplan•12mo ago
export type RoundModel<TScoring extends ScoreSystem> = {
idx: number;
bracketID: string;
name: string;
} & (TScoring extends NumericScoreSystem ? { scoreToWin: number } : Record<never, never>);
export type RoundModel<TScoring extends ScoreSystem> = {
idx: number;
bracketID: string;
name: string;
} & (TScoring extends NumericScoreSystem ? { scoreToWin: number } : Record<never, never>);
if this gets validated as
const zRoundModel = z.object({
idx: z.number(),
bracketId: z.string(),
name: z.string(),
scoreToWin: z.number().optional()
})
const zRoundModel = z.object({
idx: z.number(),
bracketId: z.string(),
name: z.string(),
scoreToWin: z.number().optional()
})
like just make it
export type RoundModel = {
idx: number;
bracketID: string;
name: string;
scoreToWin?: number
}
export type RoundModel = {
idx: number;
bracketID: string;
name: string;
scoreToWin?: number
}
like no need for the generic then
Tom
Tom•12mo ago
thats what i had and thats what im considering going back to
whatplan
whatplan•12mo ago
because I feel like the spots where the generic matters are actually really tiny and not that big of a deal to just if check or I still feel like it can be compartmentialized better like im kinda grossed by the generic affecting the very top and very bottom of the type tree
Tom
Tom•12mo ago
i started this whole side quest because i had SIngleElimFormat and DoubleElimFormat these both work fine because i cheated a little and they have the same structure. notice how the singleElimFormat has a 1 item tuple then i added round robin which was completely different and at the same time i realized that my scoring system was a little convoluted so i was thinking 'i can make the types much more strict while im making all these changes'
whatplan
whatplan•12mo ago
I thought this earlier and I still think now it would make sense to refactor a lot of this with either extends or satisfies
Tom
Tom•12mo ago
i figured that would help me make sure that the refactor was right wdym?
whatplan
whatplan•12mo ago
hm lemme just try it out and ill get back
Tom
Tom•12mo ago
np. take your time thanks again
whatplan
whatplan•12mo ago
yep np this is fun
Tom
Tom•12mo ago
i am.... out of people who want to talk to me about typescript lol
whatplan
whatplan•12mo ago
happens ok so the conditional scoreToWin field on GameModel why is that there? does it change from the defaultScoreToWin field within the ScoreSystem?
Tom
Tom•12mo ago
yeah so the idea is
Tom
Tom•12mo ago
Tom
Tom•12mo ago
the RoundModel's scoreToWin dictates the Best of X text here in the UI I have some games that dont have rounds (they live outside the tournament bracket) so i needed to add the scoreToWin into the GameModel itslef for these in the process it also simplified the code in the normal case a bit because i no longer needed to look up the roundmodel for a game to figure out the scoreToWin
whatplan
whatplan•12mo ago
i no longer needed to look up the roundmodel to figure out the scoreToWin
so why does RoundModel need a scoreToWin then
Tom
Tom•12mo ago
thats kind of what i was saying before. I could take it out of RoundModel. but RoundOptionsModel would still need it. the RoundOptionsMoel is given by the user to tell me how to assign scoreToWin for each game based on the round
whatplan
whatplan•12mo ago
do games have rounds or do rounds have games
Tom
Tom•12mo ago
rounds have games
whatplan
whatplan•12mo ago
some games that dont have rounds
what situationd oes this come up is it avoidable?
Tom
Tom•12mo ago
the game has a round: number field that is an index into tournament.format.brackets[theBracketID].rounds[round] not really. its for a feature of the site called Exhibition Games. basically the idea is that after a tournament people might want to play a grudge match or a for-fun match its not really a part of the results, but it still gets played in the software
whatplan
whatplan•12mo ago
is there ever a scenario where games within a round have a different scoreToWin
Tom
Tom•12mo ago
no or at least if that ever changes itll be a new custom format. it wont be SingleElim or DoubleElim anymore
whatplan
whatplan•12mo ago
ok Im thinking just remove it from roundmodel if its a exhibition game simple its attached to the game if its in a round just assign every game the value from roundoptions
Tom
Tom•12mo ago
yeah. i could totally do that. but RoundOptionsModel is just as complex as RoundModel
whatplan
whatplan•12mo ago
idk ill figure it out back to lookin
Tom
Tom•12mo ago
ok. ty again ill just quickly add well no. thatll just make things more complicated nevermind
whatplan
whatplan•12mo ago
all good also small note im removing Model from all the names it just kinda bothers me personal preference ig you can add it back later if you want lol
Tom
Tom•12mo ago
yeah no thats totally fine i just do that because in my code Round is the react component
whatplan
whatplan•12mo ago
yea I get that naming is hard ok so new question so as you were saying Options is user entered data? why is it necessary to store it seperately?
Tom
Tom•12mo ago
haha. that was the bit that i was going to explain but thought it might complicate things
whatplan
whatplan•12mo ago
like
Tom
Tom•12mo ago
but here goes
whatplan
whatplan•12mo ago
ok ill let you go first
Tom
Tom•12mo ago
1 sec
whatplan
whatplan•12mo ago
no rush
Tom
Tom•12mo ago
Tom
Tom•12mo ago
this is the UI for setting up the format basically the Options object is exactly what the user entered in but i need to do a bunch of math on that to figure out the actual round names because the user might specify only the last 3 round names and then just want the rest to be named 'Round 1', 'Round 2' etc or they might specify too many round names becausr they thought they would have more people enter the tournament and so now i need to truncate the list they gave me so thats all how RoundOptions gets turned into Rounds but then theres a Settings page which lets the user change anything they setup when they created the tournament i originally tried to undo the logic to rederive the options they gave me but this ends up not being really possible because for instance if a bunch of people show up late to the tournament ive now truncated the round names they gave me so i cant reconstruct them it was much simpler and more intuitive to just save the options they gave and recalculate the round names fresh when the number of players change
whatplan
whatplan•12mo ago
so I would assume the state of the tournament is always a pure function of some given user options + game data so can you explain why you cant just take the existing game data + the new user options when changed and run them through the same construction step to get the state?
Tom
Tom•12mo ago
theoretically i could do that. however, the idea ive been sticking to in the app is that each UI component renders some piece of the tournament. so the UI that gets displayed is very close to the data thats powering it this has kept things fairly simple the whole time and i think i want to stick with that unless i get a good reason to change it
whatplan
whatplan•12mo ago
im lost with those last 2 like
i originally tried to undo the logic to rederive the options they gave me but this ends up not being really possible because for instance if a bunch of people show up late to the tournament ive now truncated the round names they gave me so i cant reconstruct them
this what does this mean
Tom
Tom•12mo ago
ok so i think a scenario would help you start a tournament 2 people show up when you create the tournament. so you enter in round names, but with only 2 entrants there is only 1 game which lives in 1 round in the UI you added 3 rounds to the array. Grand Finals, Winners Finals, Winners Semifinals but theres only 1 round so i only saved Grand Finals then 10 more people show up and want to join. You add them in, but I've lost the other round names because those rounds didnt exist at the time now i hear what youre saying, which is 'why not just save the Options and then figure out the round names on the fly?' right?
whatplan
whatplan•12mo ago
uh lemme think so its important to you to be able to add options that arent necessarily possible with the given state of the tounament, ie multiple round names with only 2 participants
Tom
Tom•12mo ago
yes exactly and there are other edge cases too
whatplan
whatplan•12mo ago
I see
Tom
Tom•12mo ago
thats rthe easiest one to understand another one is that everything the user doesn't specify gets auto-filled with Round 1, Round 2, etc
whatplan
whatplan•12mo ago
so the options are different depending ont he format right
Tom
Tom•12mo ago
but then if i try to turn that back into Options so that the user can edit them, i kind of need to guess which fields were entered by the user and which were autofilled yes
whatplan
whatplan•12mo ago
ok gimme a bit
Tom
Tom•12mo ago
np at the very minimum im kinda glad this is actually hard and im not just stupid
whatplan
whatplan•12mo ago
for round robin because theres no round names only points per win/loss/tie, is it still nessecary to keep a seperate copy of the user options?
Tom
Tom•12mo ago
no. that one could be consolidated. i was planning on making. apass at that after this RoundRobins were literally written like 4 days ago and arent quite done a lot of the motivation for the refactor was 'ok round robins are mostly in, but now they need to support ties' 'you know i never really liked how im storing scores. might as well change that' 'hey i could make this whole thing a lot more typesafe' and thats how we got here
whatplan
whatplan•12mo ago
so im a a point i was like ah so whats next and there wasnt anything so ill share
Tom
Tom•12mo ago
ok
whatplan
whatplan•12mo ago
gimme a sec
Tom
Tom•12mo ago
im excited
whatplan
whatplan•12mo ago
one last thing is the name for NamedBracket only ever used to name it "winners" and "losers" in double elim like it that its only purpose for existing
Tom
Tom•12mo ago
yeah single elim brackets dont need to be named. some places have different names for the different brackets in double elim so it configurable some people call them like 'main bracket' and 'elimination bracket' or something like that
whatplan
whatplan•12mo ago
ah I see but so instead of the tuple would bracket1: namedBracket, bracket2: namedBracket also work
Tom
Tom•12mo ago
yeah... except that theyre more convenient as arrays. just because i can loop over them easier after theyre constructed winners and losers brackets are virtually identiacal so the same logic ends up getting run on both of them
whatplan
whatplan•12mo ago
I get this but also like its two do you ever see any formats with many more brackets?
Tom
Tom•12mo ago
yes, actually. thats another reason for it, although i dont support it right now when tournaments get too large theyre generally broken up into something called 'pools' which are basically mini-brackets that feed into the larger one that way you can still fit a whole pool on a reasonably sized screen / piece of paper or whatever and then logistically you can tell the people in that pool to show up at a certain time and a different pool can come at a different time so you can run a larger tournament in a smaller space TL;DR. yes. there is a definite chance that there could be more than 2
whatplan
whatplan•12mo ago
and round robin doesnt have a bracket? I guess that makes sense
Tom
Tom•12mo ago
no round robin gets drawn compeltely differently my code doesnt work rn so i cant show you
whatplan
whatplan•12mo ago
no I undersantd
Tom
Tom•12mo ago
Tom
Tom•12mo ago
but its like this
whatplan
whatplan•12mo ago
ok so im pretty happy with where im at
whatplan
whatplan•12mo ago
GitHub
GitHub - ethanniser/helping-tom-with-ts
Contribute to ethanniser/helping-tom-with-ts development by creating an account on GitHub.
whatplan
whatplan•12mo ago
lemme know what you think
Tom
Tom•12mo ago
looking now tysm
whatplan
whatplan•12mo ago
never really been a big interface person before but I think it makes sense when making a bunch of things that follow a similar pattern
Tom
Tom•12mo ago
my code switched from interfaces to types like 2 months ago
whatplan
whatplan•12mo ago
im always type first but in this case I think the interfaces show a helpful inheritance from a base type to members of that type like with the format stuff totally not required like you could just add in the base stuff to each of the types
Tom
Tom•12mo ago
yeah
whatplan
whatplan•12mo ago
or make it a type and intersect it your choice really there interfaces bad anyway updated it
Tom
Tom•12mo ago
ok so back to the first thing i asked 😛
Tom
Tom•12mo ago
it still looks like the scoreToWin thing isnt working on Game
whatplan
whatplan•12mo ago
oh ha forgot about that just assumed it worked ill fix it
Tom
Tom•12mo ago
haha np i still dont understand why it doesnt work the typings all look good and they make sense but im still not 100% sure how i would solve some of the problems i mentioned like the scoreToWin thing but also this function:
whatplan
whatplan•12mo ago
what about it
Tom
Tom•12mo ago
function applyBracketMetadata<TScoring extends ScoreSystem>(
bracket: BracketModel<TScoring>,
games: GameModel<TScoring>[]
) {
let currentHumanGameID = 0;

for (let i = 0; i < games.length; i++) {
const game = games[i];
const round = bracket.rounds[game.round];

/* add score to win based on the round */
game.scoreToWin = round.scoreToWin; //<----------- I still dont know how to make this work

...
}
}
function applyBracketMetadata<TScoring extends ScoreSystem>(
bracket: BracketModel<TScoring>,
games: GameModel<TScoring>[]
) {
let currentHumanGameID = 0;

for (let i = 0; i < games.length; i++) {
const game = games[i];
const round = bracket.rounds[game.round];

/* add score to win based on the round */
game.scoreToWin = round.scoreToWin; //<----------- I still dont know how to make this work

...
}
}
because theres still no TScoring disciminator i can check here
whatplan
whatplan•12mo ago
did we decide that round doesnt need scoretowin anymore bc game has it
Tom
Tom•12mo ago
round doesnt. i could do that but then i'd still need to do this assignment, just from the RoundOptions instead
whatplan
whatplan•12mo ago
this function is updating the games from the options right
Tom
Tom•12mo ago
yeah well in the version above its doing it from the real bracket the flow as it stands right now is user options -> bracket -> games
whatplan
whatplan•12mo ago
I think that makes sense because if you just do options theres a disconnect between RoundOptions and the Rounds the games are in so I agree take roundoptions generate rounds pass on scoretowin to games
Tom
Tom•12mo ago
yeah thats fine. but i still cant discriminate TScoring for RoundOptions as in: ts wont like it if i do roundOpts.scoreToWin if i cant prove that TScoring is NumericScoreSystem
whatplan
whatplan•12mo ago
huh isnt that the whole point you pass the correct generic so it does know
whatplan
whatplan•12mo ago
Tom
Tom•12mo ago
yeah. i see that if i pass GameModel an AnnotatedScoreSystem then it works correctly, but i dont understand why the base version has scoreToWin in it
whatplan
whatplan•12mo ago
I mean you can set a default but i dont see why you wouldnt always want to exlictly pass the right generic but it should be inferred by your data anyway in a real function lemme write an example
Tom
Tom•12mo ago
i guess that is the point. but the discriminator for TScoring is way up at the format level so i'd need to somehow discriminate a;ll thw ay up there and keep passing that down to get to this one line of code maybe this is just me misunderstanding, but my whole idea was that most functions would use Game<ScoreSystem> because they dont care about the score system most functions would work for both
whatplan
whatplan•12mo ago
like again it should all be inferred lemme example
Tom
Tom•12mo ago
yes please because the way im picturing it. I'm going to end up at the top level with a Tournament<ScoreSystem> because thats all zod can do for me so then im gunna need to figure out what kind it is and use that for all of the functions and im not sure how to do that without a ton of duplication
whatplan
whatplan•12mo ago
ok so you should never need to call the functions with generics theyll always been inferred but because they are generic you have to do validation logic + branching to account for the possible union so here you have to explictly check whether your dealing with numeric system or not ill push example in a sec
Tom
Tom•12mo ago
ok
whatplan
whatplan•12mo ago
or option 2
Tom
Tom•12mo ago
give up? 😄 haha jk
whatplan
whatplan•12mo ago
how many functions have different logic depending on the generic you said like 3
Tom
Tom•12mo ago
on TScoring specifically like 3. although im realizing that i have a few functions that like just assign scoreToWin: 0 none of them actually do anything with the score. theyre just creating the game and then that function ^ is the thing that fills it in but i guess that would mean i'd need to make 2 versions of all those functions. 1 where scoreToWin exists and 1 where it doesnt i think or do soms type assertions
whatplan
whatplan•12mo ago
thats what I was thinking for the couple functions you need split it up im making example sit tight
Tom
Tom•12mo ago
yup, thanks again
whatplan
whatplan•12mo ago
ok im adding a field to some of the types that is essnentially a in memory representation of the generic because it disappears at runtime scoreSystem: TScoring extends NumericScoreSystem ? "numeric" : "annotated"; makes figuring out what type we have much eaiser
Tom
Tom•12mo ago
yeah thats kinda what i was thinking so youll have like a bunch of discriminators sprinkled around?
whatplan
whatplan•12mo ago
I added it to Tournament and Game but you can throw that anywhere where you need to make the determination I mean its not that bad like the other option is get rid of the generic and just make it a union type discriminated on the scoreSystem field honestly not horrible
Tom
Tom•12mo ago
make what specifically a union type?
whatplan
whatplan•12mo ago
basically youd make 2 versions of every type and from the top id be like
export type TournamentUnion =
| (TournamentFields & {
scoreSystem: "numeric";
format: NumericFormat;
games: NumericGame[];
})
| {
scoreSystem: "annotated";
format: AnnotatedFormat;
games: AnnotatedGame[];
};
export type TournamentUnion =
| (TournamentFields & {
scoreSystem: "numeric";
format: NumericFormat;
games: NumericGame[];
})
| {
scoreSystem: "annotated";
format: AnnotatedFormat;
games: AnnotatedGame[];
};
same thing as generic but a bit less flexible actually no because youd only need to make 2 versions for the types that are different the rest can be shared do you like how this looks ill rewrite the whole thing wont take that long tbh
Tom
Tom•12mo ago
kinda..... i think being able to discriminate at any level would solve a lot of my problems and i feel like i understand unions better than generics
whatplan
whatplan•12mo ago
like lemme just try it out
Tom
Tom•12mo ago
yeah. please do this would also play a lot nicer with zod i have a better feeling about this
whatplan
whatplan•12mo ago
GitHub
GitHub - ethanniser/helping-tom-with-ts at union
Contribute to ethanniser/helping-tom-with-ts development by creating an account on GitHub.
whatplan
whatplan•12mo ago
zero generics now
Tom
Tom•12mo ago
looking
whatplan
whatplan•12mo ago
refresh i forgot to push and you can add discrimination whereever you need to discriminate
Tom
Tom•12mo ago
haha np
whatplan
whatplan•12mo ago
but for now I just left it only where the data is actually different
Tom
Tom•12mo ago
yeah. i like this more this will be way easier to zod the only thing i can think of which isnt really a deal breaker but its that technically the array types could contain mixed valiues like:
interface BaseBracket {
type: "anon_bracket" | "named_bracket";
id: string;
rounds: Round[];
userOptions: RoundOptions[];
}
interface BaseBracket {
type: "anon_bracket" | "named_bracket";
id: string;
rounds: Round[];
userOptions: RoundOptions[];
}
rounds could have a combo of annotated and numeric rounds but i thinkt thats not that big of a deal thats pretty hard for the code to screw up
whatplan
whatplan•12mo ago
interface BaseBracket {
type: "anon_bracket" | "named_bracket";
id: string;
rounds: AnnotatedRound[] | NumericRound[];
userOptions: AnnotatedRoundOptions[] | NumericRoundOptions[];
}
interface BaseBracket {
type: "anon_bracket" | "named_bracket";
id: string;
rounds: AnnotatedRound[] | NumericRound[];
userOptions: AnnotatedRoundOptions[] | NumericRoundOptions[];
}
Tom
Tom•12mo ago
ah yeah. that could work yeah that should totally work
whatplan
whatplan•12mo ago
especially without generics now I would really strongly consider going zod first and inferring all your types from your schemas https://transform.tools/typescript-to-zod this will make the transition pretty easy
TypeScript to Zod Schema
An online playground to convert TypeScript to Zod Schema
Tom
Tom•12mo ago
oh wow lol that would help a lot ive been doing it manually
whatplan
whatplan•12mo ago
import { z } from "zod"

export const annotatedRoundSchema = z.object({
scoreSystem: z.literal("annotated"),
idx: z.number(),
bracketID: z.string(),
name: z.string()
})

export const numericRoundSchema = z.object({
scoreSystem: z.literal("numeric"),
idx: z.number(),
bracketID: z.string(),
name: z.string(),
scoreToWin: z.number()
})

export const annotatedRoundOptionsSchema = z.object({
scoreSystem: z.literal("annotated"),
name: z.string()
})

export const numericRoundOptionsSchema = z.object({
scoreSystem: z.literal("numeric"),
name: z.string(),
scoreToWin: z.number()
})

const baseBracketSchema = z.object({
type: z.union([z.literal("anon_bracket"), z.literal("named_bracket")]),
id: z.string(),
rounds: z.union([z.array(annotatedRoundSchema), z.array(numericRoundSchema)]),
userOptions: z.union([
z.array(annotatedRoundOptionsSchema),
z.array(numericRoundOptionsSchema)
])
})

export const anonBracketSchema = baseBracketSchema.extend({
type: z.literal("anon_bracket")
})

export const namedBracketSchema = baseBracketSchema.extend({
type: z.literal("named_bracket"),
name: z.string()
})

export const roundSchema = z.union([annotatedRoundSchema, numericRoundSchema])

export const roundOptionsSchema = z.union([
annotatedRoundOptionsSchema,
numericRoundOptionsSchema
])

export const bracketSchema = z.union([anonBracketSchema, namedBracketSchema])
import { z } from "zod"

export const annotatedRoundSchema = z.object({
scoreSystem: z.literal("annotated"),
idx: z.number(),
bracketID: z.string(),
name: z.string()
})

export const numericRoundSchema = z.object({
scoreSystem: z.literal("numeric"),
idx: z.number(),
bracketID: z.string(),
name: z.string(),
scoreToWin: z.number()
})

export const annotatedRoundOptionsSchema = z.object({
scoreSystem: z.literal("annotated"),
name: z.string()
})

export const numericRoundOptionsSchema = z.object({
scoreSystem: z.literal("numeric"),
name: z.string(),
scoreToWin: z.number()
})

const baseBracketSchema = z.object({
type: z.union([z.literal("anon_bracket"), z.literal("named_bracket")]),
id: z.string(),
rounds: z.union([z.array(annotatedRoundSchema), z.array(numericRoundSchema)]),
userOptions: z.union([
z.array(annotatedRoundOptionsSchema),
z.array(numericRoundOptionsSchema)
])
})

export const anonBracketSchema = baseBracketSchema.extend({
type: z.literal("anon_bracket")
})

export const namedBracketSchema = baseBracketSchema.extend({
type: z.literal("named_bracket"),
name: z.string()
})

export const roundSchema = z.union([annotatedRoundSchema, numericRoundSchema])

export const roundOptionsSchema = z.union([
annotatedRoundOptionsSchema,
numericRoundOptionsSchema
])

export const bracketSchema = z.union([anonBracketSchema, namedBracketSchema])
Tom
Tom•12mo ago
its not a huge deal because the way my code works all the validators need to match or else there will be type errors everywhere
whatplan
whatplan•12mo ago
yea its pretty sweet yea I mean whatever works best for you
Tom
Tom•12mo ago
but this will definitely simplify it and ive been meaning to make zod the one dependency of the lib anyway
whatplan
whatplan•12mo ago
nothing wrong with dependencies
Tom
Tom•12mo ago
this site needs to be dark mode though its 4am im blind oh it has it awesome everything is right with the world
whatplan
whatplan•12mo ago
well are we done?
Tom
Tom•12mo ago
i think so. i should go to bed but im gunna try this as soonas i get up tomorrow fr though tysm
whatplan
whatplan•12mo ago
of course glad I could help
Tom
Tom•12mo ago
can i like venmo you for at least a coffee or something?
whatplan
whatplan•12mo ago
your good man im serious this was fun
Tom
Tom•12mo ago
alright well... thanks a lot
whatplan
whatplan•12mo ago
yep best of luck with the app
Tom
Tom•12mo ago
appreciate it have a good night
whatplan
whatplan•12mo ago
cheers
Tom
Tom•12mo ago
@whatplan (Rustular Devrel) thanks again for all the help last night unfortunately im still having some issues. so far most of them look like this:
whatplan
whatplan•12mo ago
Matt Pocock (@mattpocockuk)
Time to celebrate - an EXTREMELY annoying, long-standing bug will be fixed in TypeScript 5.2. Finally, using array methods on a union of arrays will no longer break!
Likes
659
From Matt Pocock (@mattpocockuk)
Twitter
whatplan
whatplan•12mo ago
yea this is just a weird ts thing aparently getting fixed next release
Tom
Tom•12mo ago
lol.... that was literally 2 days ago oy. im kinda starting to lean towards just doing it all with unit tests which i hate i think the generic approach would have been great if only i could say if TScoring === NumericScoreSystem well also it still would break zod the union thing works but again i have this issue that the differences at the lowest level of my components require the typing to be everywhere in both my code and in the actual data itself like in a tournament with 250 games, all of this typing will actually inflate the data by about 7% which is annoying
whatplan
whatplan•12mo ago
i mean its javascript
Tom
Tom•12mo ago
right but the model gets stored in the actual db
whatplan
whatplan•12mo ago
i mean still its like bytes I get its not as clean a couple additional columns wont kill your app not saying there isnt a better option there might be
Tom
Tom•12mo ago
yeah. and 'as clean' is relative to something we havent figured out yet which may or may not exist
whatplan
whatplan•12mo ago
I gotta do some work but ill check back tonight
Tom
Tom•12mo ago
yeah thanks for all the hlp last night
whatplan
whatplan•12mo ago
np
Tom
Tom•12mo ago
im gunna play around with it and see if i can come up with something tonight. maybe some combo of the stuff we tried