Use InstanceType for proper narrowing

This commit is contained in:
Sirz Benjie 2025-06-09 14:12:26 -05:00
parent 5f1a9f788f
commit 37af6d9c52
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E
5 changed files with 65 additions and 40 deletions

View File

@ -14,21 +14,39 @@ export type * from "#app/data/moves/move";
/** /**
* Map of move subclass names to their respective classes. * Map of move subclass names to their respective classes.
* Does not include the ChargeMove subclasses. For that, use `ChargingMoveClassMap`.
*
* @privateremarks
* The `Never` field is necessary to ensure typescript does not improperly narrow a failed `is` guard to `never`.
*
* For example, if we did not have the never, and wrote
* ```
* function Foo(move: Move) {
* if (move.is("AttackMove")) {
*
* } else if (move.is("StatusMove")) { // typescript errors on the `is`, saying that `move` is `never`
*
* }
* ```
*/ */
export type MoveClassMap = { export type MoveClassMap = {
AttackMove: typeof AttackMove; AttackMove: AttackMove;
StatusMove: typeof StatusMove; StatusMove: StatusMove;
SelfStatusMove: typeof SelfStatusMove; SelfStatusMove: SelfStatusMove;
ChargingAttackMove: typeof ChargingAttackMove;
ChargingSelfStatusMove: typeof ChargingSelfStatusMove;
ChargeMove: typeof ChargingAttackMove | typeof ChargingSelfStatusMove;
}; };
/*
* Without the `Never: never` field, typescript will
*/
/** /**
* Union type of all move subclass names * Union type of all move subclass names
*/ */
export type MoveClass = keyof MoveClassMap; export type MoveKindString = "AttackMove" | "StatusMove" | "SelfStatusMove";
/**
* Map of move attribute names to attribute instances.
*/
export type MoveAttrMap = { export type MoveAttrMap = {
[K in keyof MoveAttrConstructorMap]: InstanceType<MoveAttrConstructorMap[K]>; [K in keyof MoveAttrConstructorMap]: InstanceType<MoveAttrConstructorMap[K]>;
}; };

View File

@ -2527,17 +2527,11 @@ export class AllyStatMultiplierAbAttr extends AbAttr {
* @extends AbAttr * @extends AbAttr
*/ */
export class ExecutedMoveAbAttr extends AbAttr { export class ExecutedMoveAbAttr extends AbAttr {
canApplyExecutedMove( canApplyExecutedMove(_pokemon: Pokemon, _simulated: boolean): boolean {
_pokemon: Pokemon,
_simulated: boolean,
): boolean {
return true; return true;
} }
applyExecutedMove( applyExecutedMove(_pokemon: Pokemon, _simulated: boolean): void {}
_pokemon: Pokemon,
_simulated: boolean,
): void {}
} }
/** /**
@ -2545,7 +2539,7 @@ export class ExecutedMoveAbAttr extends AbAttr {
* @extends ExecutedMoveAbAttr * @extends ExecutedMoveAbAttr
*/ */
export class GorillaTacticsAbAttr extends ExecutedMoveAbAttr { export class GorillaTacticsAbAttr extends ExecutedMoveAbAttr {
constructor(showAbility: boolean = false) { constructor(showAbility = false) {
super(showAbility); super(showAbility);
} }
@ -7773,7 +7767,7 @@ export function applyPreAttackAbAttrs(
export function applyExecutedMoveAbAttrs( export function applyExecutedMoveAbAttrs(
attrType: Constructor<ExecutedMoveAbAttr>, attrType: Constructor<ExecutedMoveAbAttr>,
pokemon: Pokemon, pokemon: Pokemon,
simulated: boolean = false, simulated = false,
...args: any[] ...args: any[]
): void { ): void {
applyAbAttrsInternal<ExecutedMoveAbAttr>( applyAbAttrsInternal<ExecutedMoveAbAttr>(

View File

@ -446,8 +446,7 @@ export function initMoveAnim(move: MoveId): Promise<void> {
} }
} else { } else {
moveAnims.set(move, null); moveAnims.set(move, null);
const defaultMoveAnim = const defaultMoveAnim = allMoves[move].is("AttackMove")
allMoves[move].is("AttackMove")
? MoveId.TACKLE ? MoveId.TACKLE
: allMoves[move].is("SelfStatusMove") : allMoves[move].is("SelfStatusMove")
? MoveId.FOCUS_ENERGY ? MoveId.FOCUS_ENERGY

View File

@ -123,7 +123,7 @@ import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
import { MultiHitType } from "#enums/MultiHitType"; import { MultiHitType } from "#enums/MultiHitType";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves";
import { SelectBiomePhase } from "#app/phases/select-biome-phase"; import { SelectBiomePhase } from "#app/phases/select-biome-phase";
import { ChargingMove, MoveAttrMap, MoveAttrString, MoveClass, MoveClassMap } from "#app/@types/move-types"; import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types";
import { applyMoveAttrs } from "./apply-attrs"; import { applyMoveAttrs } from "./apply-attrs";
import { frenzyMissFunc, getMoveTargets } from "./move-utils"; import { frenzyMissFunc, getMoveTargets } from "./move-utils";
@ -153,10 +153,13 @@ export default abstract class Move implements Localizable {
/** /**
* Check if the move is of the given subclass without requiring `instanceof`. * Check if the move is of the given subclass without requiring `instanceof`.
* *
* Does _not_ work for {@linkcode ChargingAttackMove} and {@linkcode ChargingSelfStatusMove} subclasses. For those,
* use {@linkcode isChargingMove} instead.
*
* @param moveKind - The string name of the move to check against * @param moveKind - The string name of the move to check against
* @returns Whether this move is of the provided type. * @returns Whether this move is of the provided type.
*/ */
public abstract is<K extends keyof MoveClassMap>(moveKind: K): this is MoveClassMap[K]; public abstract is<K extends MoveKindString>(moveKind: K): this is MoveClassMap[K];
constructor(id: MoveId, type: PokemonType, category: MoveCategory, defaultMoveTarget: MoveTarget, power: number, accuracy: number, pp: number, chance: number, priority: number, generation: number) { constructor(id: MoveId, type: PokemonType, category: MoveCategory, defaultMoveTarget: MoveTarget, power: number, accuracy: number, pp: number, chance: number, priority: number, generation: number) {
this.id = id; this.id = id;
@ -976,6 +979,10 @@ export default abstract class Move implements Localizable {
} }
export class AttackMove extends Move { export class AttackMove extends Move {
/** This field does not exist at runtime and must not be used.
* Its sole purpose is to ensure that typescript is able to properly narrow when the `is` method is called.
*/
declare private _: never;
override is<K extends keyof MoveClassMap>(moveKind: K): this is MoveClassMap[K] { override is<K extends keyof MoveClassMap>(moveKind: K): this is MoveClassMap[K] {
return moveKind === "AttackMove"; return moveKind === "AttackMove";
} }
@ -1026,21 +1033,29 @@ export class AttackMove extends Move {
} }
export class StatusMove extends Move { export class StatusMove extends Move {
/** This field does not exist at runtime and must not be used.
* Its sole purpose is to ensure that typescript is able to properly narrow when the `is` method is called.
*/
declare private _: never;
constructor(id: MoveId, type: PokemonType, accuracy: number, pp: number, chance: number, priority: number, generation: number) { constructor(id: MoveId, type: PokemonType, accuracy: number, pp: number, chance: number, priority: number, generation: number) {
super(id, type, MoveCategory.STATUS, MoveTarget.NEAR_OTHER, -1, accuracy, pp, chance, priority, generation); super(id, type, MoveCategory.STATUS, MoveTarget.NEAR_OTHER, -1, accuracy, pp, chance, priority, generation);
} }
override is<K extends keyof MoveClassMap>(moveKind: K): this is MoveClassMap[K] { override is<K extends MoveKindString>(moveKind: K): this is MoveClassMap[K] {
return moveKind === "StatusMove"; return moveKind === "StatusMove";
} }
} }
export class SelfStatusMove extends Move { export class SelfStatusMove extends Move {
/** This field does not exist at runtime and must not be used.
* Its sole purpose is to ensure that typescript is able to properly narrow when the `is` method is called.
*/
declare private _: never;
constructor(id: MoveId, type: PokemonType, accuracy: number, pp: number, chance: number, priority: number, generation: number) { constructor(id: MoveId, type: PokemonType, accuracy: number, pp: number, chance: number, priority: number, generation: number) {
super(id, type, MoveCategory.STATUS, MoveTarget.USER, -1, accuracy, pp, chance, priority, generation); super(id, type, MoveCategory.STATUS, MoveTarget.USER, -1, accuracy, pp, chance, priority, generation);
} }
override is<K extends keyof MoveClassMap>(moveKind: K): this is MoveClassMap[K] { override is<K extends MoveKindString>(moveKind: K): this is MoveClassMap[K] {
return moveKind === "SelfStatusMove"; return moveKind === "SelfStatusMove";
} }
} }
@ -1067,11 +1082,6 @@ function ChargeMove<TBase extends SubMove>(Base: TBase, nameAppend: string) {
return true; return true;
} }
override is<K extends keyof MoveClassMap>(moveKind: K): this is MoveClassMap[K] {
// Anything subclassing this is a charge move.
return moveKind === "ChargeMove" || moveKind === nameAppend || super.is(moveKind);
}
/** /**
* Sets the text to be displayed during this move's charging phase. * Sets the text to be displayed during this move's charging phase.
* References to the user Pokemon should be written as "{USER}", and * References to the user Pokemon should be written as "{USER}", and
@ -1151,14 +1161,14 @@ export abstract class MoveAttr {
public selfTarget: boolean; public selfTarget: boolean;
/** /**
* Returns whether this attribute is of the given type. * Return whether this attribute is of the given type.
* *
* @remarks * @remarks
* Used to avoid requring the caller to have imported the specific attribute type, avoiding circular dependencies. * Used to avoid requring the caller to have imported the specific attribute type, avoiding circular dependencies.
* @param attr - The attribute to check against * @param attr - The attribute to check against
* @returns Whether the attribute is an instance of the given type. * @returns Whether the attribute is an instance of the given type.
*/ */
public is<T extends MoveAttrString>(attr: T): this is MoveAttrConstructorMap[MoveAttrString] { public is<T extends MoveAttrString>(attr: T): this is MoveAttrMap[T] {
const targetAttr = MoveAttrs[attr]; const targetAttr = MoveAttrs[attr];
if (!targetAttr) { if (!targetAttr) {
return false; return false;
@ -3096,7 +3106,11 @@ export class WeatherInstantChargeAttr extends InstantChargeAttr {
} }
export class OverrideMoveEffectAttr extends MoveAttr { export class OverrideMoveEffectAttr extends MoveAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { /** This field does not exist at runtime and must not be used.
* Its sole purpose is to ensure that typescript is able to properly narrow when the `is` method is called.
*/
declare private _: never;
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return true; return true;
} }
} }
@ -8401,7 +8415,8 @@ const MoveAttrs = Object.freeze({
ResistLastMoveTypeAttr, ResistLastMoveTypeAttr,
ExposedMoveAttr, ExposedMoveAttr,
}); });
/** Map of names */
/** Map of of move attribute names to their constructors */
export type MoveAttrConstructorMap = typeof MoveAttrs; export type MoveAttrConstructorMap = typeof MoveAttrs;
export const selfStatLowerMoves: MoveId[] = []; export const selfStatLowerMoves: MoveId[] = [];

View File

@ -28,7 +28,6 @@ import {
} from "#app/data/battler-tags"; } from "#app/data/battler-tags";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
import type { MoveAttr } from "#app/data/moves/move"; import type { MoveAttr } from "#app/data/moves/move";
import { MoveEffectAttr } from "#app/data/moves/move";
import { getMoveTargets } from "#app/data/moves/move-utils"; import { getMoveTargets } from "#app/data/moves/move-utils";
import { applyFilteredMoveAttrs, applyMoveAttrs } from "#app/data/moves/apply-attrs"; import { applyFilteredMoveAttrs, applyMoveAttrs } from "#app/data/moves/apply-attrs";
import { MoveEffectTrigger } from "#enums/MoveEffectTrigger"; import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
@ -746,7 +745,7 @@ export class MoveEffectPhase extends PokemonPhase {
): void { ): void {
applyFilteredMoveAttrs( applyFilteredMoveAttrs(
(attr: MoveAttr) => (attr: MoveAttr) =>
attr instanceof MoveEffectAttr && attr.is("MoveEffectAttr") &&
attr.trigger === triggerType && attr.trigger === triggerType &&
(isNullOrUndefined(selfTarget) || attr.selfTarget === selfTarget) && (isNullOrUndefined(selfTarget) || attr.selfTarget === selfTarget) &&
(!attr.firstHitOnly || this.firstHit) && (!attr.firstHitOnly || this.firstHit) &&