There are 3 approaches.
1. Attach the index the timer is at to the timer handle and some constant in a hashtable. You can then look it up using the timer handle and the constant.
2. Compare every element of the array sequentially looking for the triggering timer. Once found stop looping and use found location as index.
3. Encode the index into the timeout of the timer as an insignificant duration. You can then retrieve the timeout when the timer expires and decode the index from it. This works because the floating point numbers used to represent timeout have data allocated for insignificant timing units. Obviously an error is added however for low period timers this is usually sub frame and can be ignored.
1 is currently the most popular method and is also probably the fastest. It came about after the type casting exploit was fixed and hashtables were added to WC3. It works perfectly for 1 to 1 mappings like you require but can be tricky for more complex mappings to avoid collision. Avoid using a data storage hash table (one that holds a lot of constant values) as that can degrade performance however there should be little problem sharing a hashtable with all abilities in the map as long as no collisions occur. Do remember that the hashtable entry has to be removed as part of the destructor process to avoid leaking mappings and potentially other problems.
2 is the lazy person approach. You can write it in a few lines of script with just simple key words and some integer operations and it will work perfectly correctly. The problem is that this approach is not scalable at all due to the O(n) complexity of linear search so can cause performance problems in speed critical code. However if multiple instances are an exception (99.9% of time is a single instance, occasionally only 2 etc) then this approach might be faster than method 1.
3 is an old hacky approach used by people like Vexorian back in the early days of WC3 during the time of the type casting exploit. Since gamecaches were used for their hashtable like functionality via a series of custom function calls (such as handlevars) and produced strings that could potentially be very unique people tried to avoid them where possible. This approach became common place for timers and was used by various timer tools for a long time. It seems to have been mostly obsoleted by hashtables since they do not introduce timing errors and probably have better performance.
I also read periodic timers need to be paused before destroyed long time ago because sometimes they expire and crash game even you destroy them.
The game does not crash as far as I am aware of. What happens is that destroying a timer does not unschedule the next execution of the callback so a destroyed timer will appear to run 1 last time. Obviously the expired timer will be null, this will propagate null and error values through the function resulting in strange behaviour and potentially a crash due to passing illegal parameters to certain natives (eg modifying a null unit).
Mostly you destroy a timer after it expires. Since one shot timers have already expired, there is no schedule for future execution at the time of destruction. On the other hand a periodic timer is scheduled for an execution in the future so when you destroy it, it will run one final time.
The solution is obvious. Pause the timer, forcing it to be unscheduled before destroying it. It should be noted that WC3 periodic timers are incomplete and very buggy. Pausing a periodic timer causes it to lose the periodic flag and become a single shot timer. Using the inbuilt save/load system will cause all periodic timers to become single shot. For these reasons it is often better to use single shot timers and restart them every time they expire, it also solves the pausing problem.
Instead of destroying timers, they can be "recycled" as part of a green coding style system. This prevents their handle indexes from being subject to reference counter leaks from the local handle bug saving you from nulling any local handles. It does pose a possible leak hazard in the case of once per session spike timer usage. For this reason you can often forget about such a system and simply just take the effort to null local timers.
Also if I destroy the timer outside the expiration function, do I have to remove the data from the hash? Or will it just overwrite the old infromation if I createa a new one?
It would be good practice to remove the data unless you are absolutely certain that it will be overwritten soon. Do remember that the handle index objects are assigned is allocated from a generic table with few restrictions so it may be possible for a handle index that was once used for a timer to now be a unit.