For a linear easing, just change the duration and pass your own ease function, which can be as simple as:
const percentage = 0.5;//here, a value between 0 and 1
selection.transition()
.duration(5000 * (1 - percentage))
.ease(t => percentage + t * (1 - percentage))
//etc...
If you have another easing, pass the modified t value to it. For instance, using d3.easeCubic:
.ease(t => d3.easeCubic(percentage + t * (1 - percentage)))
The logic here is simple: as the API explains, "for each frame that a transition is active, it invokes its tweens with an eased t-value ranging from 0 to 1". Therefore, our function just makes the transition starting at the eased value of percentage and the t value, going from 0 to 1, is used to indicate the fraction of the amount remaining (1 - percentage) that must be added. When t = 1 the whole expression percentage + t * (1 - percentage) equals 1.
Here are the demos. First, your regular transition:
const vis = d3.select("body").append('svg').attr("width", 800).attr("height", 614).style("border", "1px solid red");
var rectangle = vis.append("rect").attr("width", 100).attr("height", 70).attr("x", 0).attr("y", 50);
rectangle
.transition()
.duration(5000)
.attr("x", 250)
.attr("width", 100)
.attr("height", 100)
.style("opacity", 0.5)
.attr("fill", "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Now using percentage = 0.5, everything (positions, opacity etc...) starts at 50% of your original transition:
const vis = d3.select("body").append('svg').attr("width", 800).attr("height", 614).style("border", "1px solid red");
var rectangle = vis.append("rect").attr("width", 100).attr("height", 70).attr("x", 0).attr("y", 50);
const percentage = 0.5;
rectangle
.transition()
.duration(5000 * (1 - percentage))
.ease(t => d3.easeCubic(percentage + t * (1 - percentage)))
.attr("x", 250)
.attr("width", 100)
.attr("height", 100)
.style("opacity", 0.5)
.attr("fill", "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Finally, with percentage = 0.8. Because this is a cubic easing, the opacity/movement are almost ending:
const vis = d3.select("body").append('svg').attr("width", 800).attr("height", 614).style("border", "1px solid red");
var rectangle = vis.append("rect").attr("width", 100).attr("height", 70).attr("x", 0).attr("y", 50);
const percentage = 0.8;
rectangle
.transition()
.duration(5000 * (1 - percentage))
.ease(t => d3.easeCubic(percentage + t * (1 - percentage)))
.attr("x", 250)
.attr("width", 100)
.attr("height", 100)
.style("opacity", 0.5)
.attr("fill", "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
PS: not related, use style instead of attr for setting styles. You'll see that, in your specific case, it makes a huge difference.